terminal/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp
Leonard Hecker e49d180e13 Allow generated profiles to be deleted (#11007)
Re-enables the delete button for generated profiles in the settings UI.
Additionally fixes "Startup Profiles" to only list active profiles.

Profiles are considered deleted if they're absent from settings.json, but their
GUID has been encountered before. Or in other words, from a user's perspective:
Generated profiles are added to the settings.json automatically only once.
Thus if the user chooses to delete the profile (e.g. using the delete button)
they aren't re-added automatically and thus appear to have been deleted.

Meanwhile those generated profiles are actually only marked as "hidden"
as well as "deleted", but still exist in internal profile lists.
The "hidden" attribute hides them from all existing menus. The "deleted" one
hides them from the settings UI and prevents them from being written to disk.

It would've been preferrable of course to just not generate and
add deleted profile to internal profile lists in the first place.
But this would've required far more wide-reaching changes.
The settings UI for instance requires a list of _all_ profiles in order to
allow a user to re-create previously deleted profiles. Such an approach was
attempted but discarded because of it's current complexity overhead.

* Part of #9997
* A sequel to 5d36e5d

* [x] Closes #10960
* [x] I work here
* [x] Tests added/passed

* "Startup Profiles" doesn't list deleted profiles ✔️
* Manually removing an item from settings.json removes the profile ✔️
* Removing cmd.exe and saving doesn't create empty objects (#10960) ✔️
* "Add a new profile" lists deleted profiles ✔️
* "Duplicate" recreates previously deleted profiles ✔️
* Profiles are always created with GUIDs ✔️
2021-08-25 18:00:41 -05:00

1247 lines
47 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "CascadiaSettings.h"
#include "CascadiaSettings.g.cpp"
#include <LibraryResources.h>
#include "AzureCloudShellGenerator.h"
#include "PowershellCoreProfileGenerator.h"
#include "WslDistroGenerator.h"
using namespace ::Microsoft::Terminal::Settings::Model;
using namespace winrt::Microsoft::Terminal;
using namespace winrt::Microsoft::Terminal::Control;
using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
using namespace winrt::Windows::Foundation::Collections;
using namespace Microsoft::Console;
static constexpr std::wstring_view PACKAGED_PROFILE_ICON_PATH{ L"ms-appx:///ProfileIcons/" };
static constexpr std::wstring_view PACKAGED_PROFILE_ICON_EXTENSION{ L".png" };
static constexpr std::wstring_view DEFAULT_LINUX_ICON_GUID{ L"{9acb9455-ca41-5af7-950f-6bca1bc9722f}" };
// make sure this matches defaults.json.
static constexpr std::wstring_view DEFAULT_WINDOWS_POWERSHELL_GUID{ L"{61c54bbd-c2c6-5271-96e7-009a87ff44bf}" };
CascadiaSettings::CascadiaSettings() :
CascadiaSettings(true)
{
}
// Constructor Description:
// - Creates a new settings object. If addDynamicProfiles is true, we'll
// automatically add the built-in profile generators to our list of profile
// generators. Set this to `false` for unit testing.
// Arguments:
// - addDynamicProfiles: if true, we'll add the built-in DPGs.
CascadiaSettings::CascadiaSettings(const bool addDynamicProfiles) :
_globals{ winrt::make_self<implementation::GlobalAppSettings>() },
_allProfiles{ winrt::single_threaded_observable_vector<Model::Profile>() },
_activeProfiles{ winrt::single_threaded_observable_vector<Model::Profile>() },
_warnings{ winrt::single_threaded_vector<SettingsLoadWarnings>() },
_deserializationErrorMessage{ L"" },
_defaultTerminals{ winrt::single_threaded_observable_vector<Model::DefaultTerminal>() },
_currentDefaultTerminal{ nullptr }
{
if (addDynamicProfiles)
{
_profileGenerators.emplace_back(std::make_unique<PowershellCoreProfileGenerator>());
_profileGenerators.emplace_back(std::make_unique<WslDistroGenerator>());
_profileGenerators.emplace_back(std::make_unique<AzureCloudShellGenerator>());
}
}
CascadiaSettings::CascadiaSettings(winrt::hstring json) :
CascadiaSettings(false)
{
const auto jsonString{ til::u16u8(json) };
_ParseJsonString(jsonString, false);
LayerJson(_userSettings);
_ValidateSettings();
}
winrt::Microsoft::Terminal::Settings::Model::CascadiaSettings CascadiaSettings::Copy() const
{
// dynamic profile generators added by default
auto settings{ winrt::make_self<CascadiaSettings>() };
settings->_globals = _globals->Copy();
for (auto warning : _warnings)
{
settings->_warnings.Append(warning);
}
settings->_loadError = _loadError;
settings->_deserializationErrorMessage = _deserializationErrorMessage;
settings->_userSettingsString = _userSettingsString;
settings->_userSettings = _userSettings;
settings->_defaultSettings = _defaultSettings;
settings->_defaultTerminals = _defaultTerminals;
settings->_currentDefaultTerminal = _currentDefaultTerminal;
_CopyProfileInheritanceTree(settings);
return *settings;
}
// Method Description:
// - Copies the inheritance tree for profiles and hooks them up to a clone CascadiaSettings
// Arguments:
// - cloneSettings: the CascadiaSettings we're copying the inheritance tree to
// Return Value:
// - <none>
void CascadiaSettings::_CopyProfileInheritanceTree(winrt::com_ptr<CascadiaSettings>& cloneSettings) const
{
// Our profiles inheritance graph doesn't have a formal root.
// However, if we create a dummy Profile, and set _profiles as the parent,
// we now have a root. So we'll do just that, then copy the inheritance graph
// from the dummyRoot.
auto dummyRootSource{ winrt::make_self<Profile>() };
for (const auto& profile : _allProfiles)
{
winrt::com_ptr<Profile> profileImpl;
profileImpl.copy_from(winrt::get_self<Profile>(profile));
Profile::InsertParentHelper(dummyRootSource, profileImpl);
}
auto dummyRootClone{ winrt::make_self<Profile>() };
std::unordered_map<void*, winrt::com_ptr<Profile>> visited{};
if (_userDefaultProfileSettings)
{
// profile.defaults must be saved to CascadiaSettings
// So let's do that manually first, and add that to visited
cloneSettings->_userDefaultProfileSettings = Profile::CopySettings(_userDefaultProfileSettings);
visited[_userDefaultProfileSettings.get()] = cloneSettings->_userDefaultProfileSettings;
}
Profile::CloneInheritanceGraph(dummyRootSource, dummyRootClone, visited);
// All of the parents of the dummy root clone are _profiles.
// Get the parents and add them to the settings clone.
const auto cloneParents{ dummyRootClone->Parents() };
for (const auto& profile : cloneParents)
{
cloneSettings->_allProfiles.Append(*profile);
if (!profile->Hidden())
{
cloneSettings->_activeProfiles.Append(*profile);
}
}
}
// Method Description:
// - Finds a profile that matches the given GUID. If there is no profile in this
// settings object that matches, returns nullptr.
// Arguments:
// - profileGuid: the GUID of the profile to return.
// Return Value:
// - a non-ownership pointer to the profile matching the given guid, or nullptr
// if there is no match.
winrt::Microsoft::Terminal::Settings::Model::Profile CascadiaSettings::FindProfile(winrt::guid profileGuid) const noexcept
{
const winrt::guid guid{ profileGuid };
for (const auto& profile : _allProfiles)
{
try
{
if (profile.Guid() == guid)
{
return profile;
}
}
CATCH_LOG();
}
return nullptr;
}
// Method Description:
// - Returns an iterable collection of all of our Profiles.
// Arguments:
// - <none>
// Return Value:
// - an iterable collection of all of our Profiles.
IObservableVector<winrt::Microsoft::Terminal::Settings::Model::Profile> CascadiaSettings::AllProfiles() const noexcept
{
return _allProfiles;
}
// Method Description:
// - Returns an iterable collection of all of our non-hidden Profiles.
// Arguments:
// - <none>
// Return Value:
// - an iterable collection of all of our Profiles.
IObservableVector<winrt::Microsoft::Terminal::Settings::Model::Profile> CascadiaSettings::ActiveProfiles() const noexcept
{
return _activeProfiles;
}
// Method Description:
// - Returns the globally configured keybindings
// Arguments:
// - <none>
// Return Value:
// - the globally configured keybindings
winrt::Microsoft::Terminal::Settings::Model::ActionMap CascadiaSettings::ActionMap() const noexcept
{
return _globals->ActionMap();
}
// Method Description:
// - Get a reference to our global settings
// Arguments:
// - <none>
// Return Value:
// - a reference to our global settings
winrt::Microsoft::Terminal::Settings::Model::GlobalAppSettings CascadiaSettings::GlobalSettings() const
{
return *_globals;
}
// Method Description:
// - Get a reference to our profiles.defaults object
// Arguments:
// - <none>
// Return Value:
// - a reference to our profile.defaults object
winrt::Microsoft::Terminal::Settings::Model::Profile CascadiaSettings::ProfileDefaults() const
{
return *_userDefaultProfileSettings;
}
// Method Description:
// - Create a new profile based off the default profile settings.
// Arguments:
// - <none>
// Return Value:
// - a reference to the new profile
winrt::Microsoft::Terminal::Settings::Model::Profile CascadiaSettings::CreateNewProfile()
{
if (_allProfiles.Size() == std::numeric_limits<uint32_t>::max())
{
// Shouldn't really happen
return nullptr;
}
std::wstring newName;
for (uint32_t candidateIndex = 0, count = _allProfiles.Size() + 1; candidateIndex < count; candidateIndex++)
{
// There is a theoretical unsigned integer wraparound, which is OK
newName = fmt::format(L"Profile {}", count + candidateIndex);
if (std::none_of(begin(_allProfiles), end(_allProfiles), [&](auto&& profile) { return profile.Name() == newName; }))
{
break;
}
}
const auto newProfile = _CreateNewProfile(newName);
_allProfiles.Append(*newProfile);
_activeProfiles.Append(*newProfile);
return *newProfile;
}
// Method Description:
// - Duplicate a new profile based off another profile's settings
// - This differs from Profile::Copy because it also copies over settings
// that were not defined in the json (for example, settings that were
// defined in one of the parents)
// - This will not duplicate settings that were defined in profiles.defaults
// however, because we do not want the json blob generated from the new profile
// to contain those settings
// Arguments:
// - source: the Profile object we are duplicating (must not be null)
// Return Value:
// - a reference to the new profile
winrt::Microsoft::Terminal::Settings::Model::Profile CascadiaSettings::DuplicateProfile(const Model::Profile& source)
{
THROW_HR_IF_NULL(E_INVALIDARG, source);
auto newName = fmt::format(L"{} ({})", source.Name(), RS_(L"CopySuffix"));
// Check if this name already exists and if so, append a number
for (uint32_t candidateIndex = 0, count = _allProfiles.Size() + 1; candidateIndex < count; ++candidateIndex)
{
if (std::none_of(begin(_allProfiles), end(_allProfiles), [&](auto&& profile) { return profile.Name() == newName; }))
{
break;
}
// There is a theoretical unsigned integer wraparound, which is OK
newName = fmt::format(L"{} ({} {})", source.Name(), RS_(L"CopySuffix"), candidateIndex + 2);
}
const auto duplicated = _CreateNewProfile(newName);
static constexpr auto isProfilesDefaultsOrigin = [](const auto& profile) -> bool {
return profile && profile.Origin() != OriginTag::ProfilesDefaults;
};
static constexpr auto isProfilesDefaultsOriginSub = [](const auto& sub) -> bool {
return sub && isProfilesDefaultsOrigin(sub.SourceProfile());
};
#define DUPLICATE_SETTING_MACRO(settingName) \
if (source.Has##settingName() || isProfilesDefaultsOrigin(source.settingName##OverrideSource())) \
{ \
duplicated->settingName(source.settingName()); \
}
#define DUPLICATE_SETTING_MACRO_SUB(source, target, settingName) \
if (source.Has##settingName() || isProfilesDefaultsOriginSub(source.settingName##OverrideSource())) \
{ \
target.settingName(source.settingName()); \
}
// If the source is hidden and the Settings UI creates a
// copy of it we don't want the copy to be hidden as well.
// --> Don't do DUPLICATE_SETTING_MACRO(Hidden);
DUPLICATE_SETTING_MACRO(Icon);
DUPLICATE_SETTING_MACRO(CloseOnExit);
DUPLICATE_SETTING_MACRO(TabTitle);
DUPLICATE_SETTING_MACRO(TabColor);
DUPLICATE_SETTING_MACRO(SuppressApplicationTitle);
DUPLICATE_SETTING_MACRO(UseAcrylic);
DUPLICATE_SETTING_MACRO(AcrylicOpacity);
DUPLICATE_SETTING_MACRO(ScrollState);
DUPLICATE_SETTING_MACRO(Padding);
DUPLICATE_SETTING_MACRO(Commandline);
DUPLICATE_SETTING_MACRO(StartingDirectory);
DUPLICATE_SETTING_MACRO(AntialiasingMode);
DUPLICATE_SETTING_MACRO(ForceFullRepaintRendering);
DUPLICATE_SETTING_MACRO(SoftwareRendering);
DUPLICATE_SETTING_MACRO(HistorySize);
DUPLICATE_SETTING_MACRO(SnapOnInput);
DUPLICATE_SETTING_MACRO(AltGrAliasing);
DUPLICATE_SETTING_MACRO(BellStyle);
{
const auto font = source.FontInfo();
auto target = duplicated->FontInfo();
DUPLICATE_SETTING_MACRO_SUB(font, target, FontFace);
DUPLICATE_SETTING_MACRO_SUB(font, target, FontSize);
DUPLICATE_SETTING_MACRO_SUB(font, target, FontWeight);
}
{
const auto appearance = source.DefaultAppearance();
auto target = duplicated->DefaultAppearance();
DUPLICATE_SETTING_MACRO_SUB(appearance, target, ColorSchemeName);
DUPLICATE_SETTING_MACRO_SUB(appearance, target, Foreground);
DUPLICATE_SETTING_MACRO_SUB(appearance, target, Background);
DUPLICATE_SETTING_MACRO_SUB(appearance, target, SelectionBackground);
DUPLICATE_SETTING_MACRO_SUB(appearance, target, CursorColor);
DUPLICATE_SETTING_MACRO_SUB(appearance, target, PixelShaderPath);
DUPLICATE_SETTING_MACRO_SUB(appearance, target, BackgroundImagePath);
DUPLICATE_SETTING_MACRO_SUB(appearance, target, BackgroundImageOpacity);
DUPLICATE_SETTING_MACRO_SUB(appearance, target, BackgroundImageStretchMode);
DUPLICATE_SETTING_MACRO_SUB(appearance, target, BackgroundImageAlignment);
DUPLICATE_SETTING_MACRO_SUB(appearance, target, RetroTerminalEffect);
DUPLICATE_SETTING_MACRO_SUB(appearance, target, CursorShape);
DUPLICATE_SETTING_MACRO_SUB(appearance, target, CursorHeight);
DUPLICATE_SETTING_MACRO_SUB(appearance, target, IntenseTextStyle);
}
// UnfocusedAppearance is treated as a single setting,
// but requires a little more legwork to duplicate properly
if (source.HasUnfocusedAppearance() ||
(source.UnfocusedAppearanceOverrideSource() != nullptr && source.UnfocusedAppearanceOverrideSource().Origin() != OriginTag::ProfilesDefaults))
{
// First, get a com_ptr to the source's unfocused appearance
// We need this to be able to call CopyAppearance (it is alright to simply call CopyAppearance here
// instead of needing a separate function like DuplicateAppearance since UnfocusedAppearance is treated
// as a single setting)
winrt::com_ptr<AppearanceConfig> sourceUnfocusedAppearanceImpl;
sourceUnfocusedAppearanceImpl.copy_from(winrt::get_self<AppearanceConfig>(source.UnfocusedAppearance()));
// Get a weak ref to the duplicate profile so we can provide a source profile to the new UnfocusedAppearance
// we are about to create
const auto weakRefToDuplicated = weak_ref<Model::Profile>(*duplicated);
auto duplicatedUnfocusedAppearanceImpl = AppearanceConfig::CopyAppearance(sourceUnfocusedAppearanceImpl, weakRefToDuplicated);
// Make sure to add the default appearance of the duplicated profile as a parent to the duplicate's UnfocusedAppearance
winrt::com_ptr<AppearanceConfig> duplicatedDefaultAppearanceImpl;
duplicatedDefaultAppearanceImpl.copy_from(winrt::get_self<AppearanceConfig>(duplicated->DefaultAppearance()));
duplicatedUnfocusedAppearanceImpl->InsertParent(duplicatedDefaultAppearanceImpl);
// Finally, set the duplicate's UnfocusedAppearance
duplicated->UnfocusedAppearance(*duplicatedUnfocusedAppearanceImpl);
}
if (source.HasConnectionType())
{
duplicated->ConnectionType(source.ConnectionType());
}
_allProfiles.Append(*duplicated);
_activeProfiles.Append(*duplicated);
return *duplicated;
}
// Method Description:
// - Gets our list of warnings we found during loading. These are things that we
// knew were bad when we called `_ValidateSettings` last.
// Return Value:
// - a reference to our list of warnings.
IVectorView<winrt::Microsoft::Terminal::Settings::Model::SettingsLoadWarnings> CascadiaSettings::Warnings()
{
return _warnings.GetView();
}
void CascadiaSettings::ClearWarnings()
{
_warnings.Clear();
}
void CascadiaSettings::AppendWarning(SettingsLoadWarnings warning)
{
_warnings.Append(warning);
}
winrt::Windows::Foundation::IReference<winrt::Microsoft::Terminal::Settings::Model::SettingsLoadErrors> CascadiaSettings::GetLoadingError()
{
return _loadError;
}
winrt::hstring CascadiaSettings::GetSerializationErrorMessage()
{
return _deserializationErrorMessage;
}
// As used by CreateNewProfile and DuplicateProfile this function
// creates a new Profile instance with a random UUID and a given name.
winrt::com_ptr<Profile> CascadiaSettings::_CreateNewProfile(const std::wstring_view& name) const
{
winrt::com_ptr<Profile> profile;
if (_userDefaultProfileSettings)
{
profile = _userDefaultProfileSettings->CreateChild();
}
else
{
profile = winrt::make_self<Profile>();
}
// Technically there's Utils::CreateV5Uuid which we could use, but I wanted
// truly globally unique UUIDs for profiles created through the settings UI.
GUID guid{};
LOG_IF_FAILED(CoCreateGuid(&guid));
profile->Guid(guid);
profile->Name(winrt::hstring{ name });
return profile;
}
// Method Description:
// - Attempts to validate this settings structure. If there are critical errors
// found, they'll be thrown as a SettingsLoadError. Non-critical errors, such
// as not finding the default profile, will only result in an error. We'll add
// all these warnings to our list of warnings, and the application can chose
// to display these to the user.
// Arguments:
// - <none>
// Return Value:
// - <none>
void CascadiaSettings::_ValidateSettings()
{
// Make sure to check that profiles exists at all first and foremost:
_ValidateProfilesExist();
// Re-order profiles so that all profiles from the user's settings appear
// before profiles that _weren't_ in the user profiles.
_ReorderProfilesToMatchUserSettingsOrder();
// Remove hidden profiles _after_ re-ordering. The re-ordering uses the raw
// json, and will get confused if the profile isn't in the list.
_UpdateActiveProfiles();
// Then do some validation on the profiles. The order of these does not
// terribly matter.
_ValidateNoDuplicateProfiles();
// Resolve the default profile before we validate that it exists.
_ResolveDefaultProfile();
_ValidateDefaultProfileExists();
// Ensure that all the profile's color scheme names are
// actually the names of schemes we've parsed. If the scheme doesn't exist,
// just use the hardcoded defaults
_ValidateAllSchemesExist();
// Ensure all profile's with specified images resources have valid file path.
// This validates icons and background images.
_ValidateMediaResources();
// TODO:GH#2548 ensure there's at least one key bound. Display a warning if
// there's _NO_ keys bound to any actions. That's highly irregular, and
// likely an indication of an error somehow.
// GH#3522 - With variable args to keybindings, it's possible that a user
// set a keybinding without all the required args for an action. Display a
// warning if an action didn't have a required arg.
// This will also catch other keybinding warnings, like from GH#4239
_ValidateKeybindings();
_ValidateColorSchemesInCommands();
_ValidateNoGlobalsKey();
}
// Method Description:
// - Checks if the settings contain profiles at all. As we'll need to have some
// profiles at all, we'll throw an error if there aren't any profiles.
void CascadiaSettings::_ValidateProfilesExist()
{
const bool hasProfiles = _allProfiles.Size() > 0;
if (!hasProfiles)
{
// Throw an exception. This is an invalid state, and we want the app to
// be able to gracefully use the default settings.
// We can't add the warning to the list of warnings here, because this
// object is not going to be returned at any point.
throw SettingsException(Microsoft::Terminal::Settings::Model::SettingsLoadErrors::NoProfiles);
}
}
// Method Description:
// - Resolves the "defaultProfile", which can be a profile name, to a GUID
// and stores it back to the globals.
void CascadiaSettings::_ResolveDefaultProfile()
{
const auto unparsedDefaultProfile{ GlobalSettings().UnparsedDefaultProfile() };
if (!unparsedDefaultProfile.empty())
{
auto maybeParsedDefaultProfile{ _GetProfileGuidByName(unparsedDefaultProfile) };
auto defaultProfileGuid{ til::coalesce_value(maybeParsedDefaultProfile, winrt::guid{}) };
GlobalSettings().DefaultProfile(defaultProfileGuid);
}
}
// Method Description:
// - Checks if the "defaultProfile" is set to one of the profiles we
// actually have. If the value is unset, or the value is set to something that
// doesn't exist in the list of profiles, we'll arbitrarily pick the first
// profile to use temporarily as the default.
// - Appends a SettingsLoadWarnings::MissingDefaultProfile to our list of
// warnings if we failed to find the default.
void CascadiaSettings::_ValidateDefaultProfileExists()
{
const winrt::guid defaultProfileGuid{ GlobalSettings().DefaultProfile() };
const bool nullDefaultProfile = defaultProfileGuid == winrt::guid{};
bool defaultProfileNotInProfiles = true;
for (const auto& profile : _allProfiles)
{
if (profile.Guid() == defaultProfileGuid)
{
defaultProfileNotInProfiles = false;
break;
}
}
if (nullDefaultProfile || defaultProfileNotInProfiles)
{
_warnings.Append(Microsoft::Terminal::Settings::Model::SettingsLoadWarnings::MissingDefaultProfile);
// Use the first profile as the new default
// _temporarily_ set the default profile to the first profile. Because
// we're adding a warning, this settings change won't be re-serialized.
GlobalSettings().DefaultProfile(_allProfiles.GetAt(0).Guid());
}
}
// Method Description:
// - Checks to make sure there aren't any duplicate profiles in the list of
// profiles. If so, we'll remove the subsequent entries (temporarily), as they
// won't be accessible anyways.
// - Appends a SettingsLoadWarnings::DuplicateProfile to our list of warnings if
// we find any such duplicate.
void CascadiaSettings::_ValidateNoDuplicateProfiles()
{
bool foundDupe = false;
std::vector<uint32_t> indicesToDelete;
std::set<winrt::guid> uniqueGuids;
// Try collecting all the unique guids. If we ever encounter a guid that's
// already in the set, then we need to delete that profile.
for (uint32_t i = 0; i < _allProfiles.Size(); i++)
{
if (!uniqueGuids.insert(_allProfiles.GetAt(i).Guid()).second)
{
foundDupe = true;
indicesToDelete.push_back(i);
}
}
// Remove all the duplicates we've marked
// Walk backwards, so we don't accidentally shift any of the elements
for (auto iter = indicesToDelete.rbegin(); iter != indicesToDelete.rend(); iter++)
{
_allProfiles.RemoveAt(*iter);
}
if (foundDupe)
{
_warnings.Append(Microsoft::Terminal::Settings::Model::SettingsLoadWarnings::DuplicateProfile);
}
}
// Method Description:
// - Re-orders the list of profiles to match what the user would expect them to
// be. Orders profiles to be in the ordering { [profiles from user settings],
// [default profiles that weren't in the user profiles]}.
// - Does not set any warnings.
// Arguments:
// - <none>
// Return Value:
// - <none>
void CascadiaSettings::_ReorderProfilesToMatchUserSettingsOrder()
{
std::set<winrt::guid> uniqueGuids;
std::deque<winrt::guid> guidOrder;
auto collectGuids = [&](const auto& json) {
for (auto profileJson : _GetProfilesJsonObject(json))
{
if (profileJson.isObject())
{
auto guid = implementation::Profile::GetGuidOrGenerateForJson(profileJson);
if (uniqueGuids.insert(guid).second)
{
guidOrder.push_back(guid);
}
}
}
};
// Push all the userSettings profiles' GUIDS into the set
collectGuids(_userSettings);
// Push all the defaultSettings profiles' GUIDS into the set
collectGuids(_defaultSettings);
std::equal_to<winrt::guid> equals;
// Re-order the list of profiles to match that ordering
// for (gIndex=0 -> uniqueGuids.size)
// pIndex = the pIndex of the profile with guid==guids[gIndex]
// profiles.swap(pIndex <-> gIndex)
// This is O(N^2), which is kinda rough. I'm sure there's a better way
for (uint32_t gIndex = 0; gIndex < guidOrder.size(); gIndex++)
{
const auto guid = guidOrder.at(gIndex);
for (uint32_t pIndex = gIndex; pIndex < _allProfiles.Size(); pIndex++)
{
auto profileGuid = _allProfiles.GetAt(pIndex).Guid();
if (equals(profileGuid, guid))
{
auto prof1 = _allProfiles.GetAt(pIndex);
_allProfiles.SetAt(pIndex, _allProfiles.GetAt(gIndex));
_allProfiles.SetAt(gIndex, prof1);
break;
}
}
}
}
// Method Description:
// - Updates the list of active profiles from the list of all profiles
// - If there are no active profiles (all profiles are hidden), throw a SettingsException
// - Does not set any warnings.
// Arguments:
// - <none>
// Return Value:
// - <none>
void CascadiaSettings::_UpdateActiveProfiles()
{
_activeProfiles.Clear();
for (auto const& profile : _allProfiles)
{
if (!profile.Hidden())
{
_activeProfiles.Append(profile);
}
}
// Ensure that we still have some profiles here. If we don't, then throw an
// exception, so the app can use the defaults.
const bool hasProfiles = _activeProfiles.Size() > 0;
if (!hasProfiles)
{
// Throw an exception. This is an invalid state, and we want the app to
// be able to gracefully use the default settings.
throw SettingsException(SettingsLoadErrors::AllProfilesHidden);
}
}
// Method Description:
// - Ensures that every profile has a valid "color scheme" set. If any profile
// has a colorScheme set to a value which is _not_ the name of an actual color
// scheme, we'll set the color table of the profile to something reasonable.
// Arguments:
// - <none>
// Return Value:
// - <none>
// - Appends a SettingsLoadWarnings::UnknownColorScheme to our list of warnings if
// we find any such duplicate.
void CascadiaSettings::_ValidateAllSchemesExist()
{
bool foundInvalidScheme = false;
for (auto profile : _allProfiles)
{
const auto schemeName = profile.DefaultAppearance().ColorSchemeName();
if (!_globals->ColorSchemes().HasKey(schemeName))
{
// Clear the user set color scheme. We'll just fallback instead.
profile.DefaultAppearance().ClearColorSchemeName();
foundInvalidScheme = true;
}
if (profile.UnfocusedAppearance())
{
const auto unfocusedSchemeName = profile.UnfocusedAppearance().ColorSchemeName();
if (!_globals->ColorSchemes().HasKey(unfocusedSchemeName))
{
profile.UnfocusedAppearance().ClearColorSchemeName();
foundInvalidScheme = true;
}
}
}
if (foundInvalidScheme)
{
_warnings.Append(SettingsLoadWarnings::UnknownColorScheme);
}
}
// Method Description:
// - Ensures that all specified images resources (icons and background images) are valid URIs.
// This does not verify that the icon or background image files are encoded as an image.
// Arguments:
// - <none>
// Return Value:
// - <none>
// - Appends a SettingsLoadWarnings::InvalidBackgroundImage to our list of warnings if
// we find any invalid background images.
// - Appends a SettingsLoadWarnings::InvalidIconImage to our list of warnings if
// we find any invalid icon images.
void CascadiaSettings::_ValidateMediaResources()
{
bool invalidBackground{ false };
bool invalidIcon{ false };
for (auto profile : _allProfiles)
{
if (!profile.DefaultAppearance().BackgroundImagePath().empty())
{
// Attempt to convert the path to a URI, the ctor will throw if it's invalid/unparseable.
// This covers file paths on the machine, app data, URLs, and other resource paths.
try
{
winrt::Windows::Foundation::Uri imagePath{ profile.DefaultAppearance().ExpandedBackgroundImagePath() };
}
catch (...)
{
// reset background image path
profile.DefaultAppearance().BackgroundImagePath(L"");
invalidBackground = true;
}
}
if (profile.UnfocusedAppearance())
{
if (!profile.UnfocusedAppearance().BackgroundImagePath().empty())
{
// Attempt to convert the path to a URI, the ctor will throw if it's invalid/unparseable.
// This covers file paths on the machine, app data, URLs, and other resource paths.
try
{
winrt::Windows::Foundation::Uri imagePath{ profile.UnfocusedAppearance().ExpandedBackgroundImagePath() };
}
catch (...)
{
// reset background image path
profile.UnfocusedAppearance().BackgroundImagePath(L"");
invalidBackground = true;
}
}
}
if (!profile.Icon().empty())
{
const auto iconPath{ wil::ExpandEnvironmentStringsW<std::wstring>(profile.Icon().c_str()) };
try
{
winrt::Windows::Foundation::Uri imagePath{ iconPath };
}
catch (...)
{
// Anything longer than 2 wchar_t's _isn't_ an emoji or symbol,
// so treat it as an invalid path.
if (iconPath.size() > 2)
{
// reset icon path
profile.Icon(L"");
invalidIcon = true;
}
}
}
}
if (invalidBackground)
{
_warnings.Append(SettingsLoadWarnings::InvalidBackgroundImage);
}
if (invalidIcon)
{
_warnings.Append(SettingsLoadWarnings::InvalidIcon);
}
}
// Method Description:
// - Helper to get the GUID of a profile, given an optional index and a possible
// "profile" value to override that.
// - First, we'll try looking up the profile for the given index. This will
// either get us the GUID of the Nth profile, or the GUID of the default
// profile.
// - Then, if there was a Profile set in the NewTerminalArgs, we'll use that to
// try and look the profile up by either GUID or name.
// Arguments:
// - index: if provided, the index in the list of profiles to get the GUID for.
// If omitted, instead use the default profile's GUID
// - newTerminalArgs: An object that may contain a profile name or GUID to
// actually use. If the Profile value is not a guid, we'll treat it as a name,
// and attempt to look the profile up by name instead.
// Return Value:
// - the GUID of the profile corresponding to this combination of index and NewTerminalArgs
winrt::guid CascadiaSettings::GetProfileForArgs(const Model::NewTerminalArgs& newTerminalArgs) const
{
std::optional<winrt::guid> profileByIndex, profileByName;
if (newTerminalArgs)
{
if (newTerminalArgs.ProfileIndex() != nullptr)
{
profileByIndex = _GetProfileGuidByIndex(newTerminalArgs.ProfileIndex().Value());
}
profileByName = _GetProfileGuidByName(newTerminalArgs.Profile());
}
return til::coalesce_value(profileByName, profileByIndex, _globals->DefaultProfile());
}
// Method Description:
// - Helper to get the GUID of a profile given a name that could be a guid or an actual name.
// Arguments:
// - name: a guid string _or_ the name of a profile
// Return Value:
// - the GUID of the profile corresponding to this name
std::optional<winrt::guid> CascadiaSettings::_GetProfileGuidByName(const winrt::hstring name) const
try
{
// First, try and parse the "name" as a GUID. If it's a
// GUID, and the GUID of one of our profiles, then use that as the
// profile GUID instead. If it's not, then try looking it up as a
// name of a profile. If it's still not that, then just ignore it.
if (!name.empty())
{
// Do a quick heuristic check - is the profile 38 chars long (the
// length of a GUID string), and does it start with '{'? Because if
// it doesn't, it's _definitely_ not a GUID.
if (name.size() == 38 && name[0] == L'{')
{
const auto newGUID{ Utils::GuidFromString(static_cast<std::wstring>(name)) };
if (FindProfile(newGUID))
{
return newGUID;
}
}
// Here, we were unable to use the profile string as a GUID to
// lookup a profile. Instead, try using the string to look the
// Profile up by name.
for (auto profile : _allProfiles)
{
if (profile.Name() == name)
{
return profile.Guid();
}
}
}
return std::nullopt;
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
return std::nullopt;
}
// Method Description:
// - Helper to find the profile GUID for a the profile at the given index in the
// list of profiles. If no index is provided, this instead returns the default
// profile's guid. This is used by the NewTabProfile<N> ShortcutActions to
// create a tab for the Nth profile in the list of profiles.
// Arguments:
// - index: if provided, the index in the list of profiles to get the GUID for.
// If omitted, instead return the default profile's GUID
// Return Value:
// - the Nth profile's GUID, or the default profile's GUID
std::optional<winrt::guid> CascadiaSettings::_GetProfileGuidByIndex(std::optional<int> index) const
{
if (index)
{
const auto realIndex{ index.value() };
// If we don't have that many profiles, then do nothing.
if (realIndex >= 0 &&
realIndex < gsl::narrow_cast<decltype(realIndex)>(_activeProfiles.Size()))
{
const auto& selectedProfile = _activeProfiles.GetAt(realIndex);
return selectedProfile.Guid();
}
}
return std::nullopt;
}
// Method Description:
// - If there were any warnings we generated while parsing the user's
// keybindings, add them to the list of warnings here. If there were warnings
// generated in this way, we'll add a AtLeastOneKeybindingWarning, which will
// act as a header for the other warnings
// Arguments:
// - <none>
// Return Value:
// - <none>
void CascadiaSettings::_ValidateKeybindings()
{
auto keybindingWarnings = _globals->KeybindingsWarnings();
if (!keybindingWarnings.empty())
{
_warnings.Append(SettingsLoadWarnings::AtLeastOneKeybindingWarning);
for (auto warning : keybindingWarnings)
{
_warnings.Append(warning);
}
}
}
// Method Description:
// - Ensures that every "setColorScheme" command has a valid "color scheme" set.
// Arguments:
// - <none>
// Return Value:
// - <none>
// - Appends a SettingsLoadWarnings::InvalidColorSchemeInCmd to our list of warnings if
// we find any command with an invalid color scheme.
void CascadiaSettings::_ValidateColorSchemesInCommands()
{
bool foundInvalidScheme{ false };
for (const auto& nameAndCmd : _globals->ActionMap().NameMap())
{
if (_HasInvalidColorScheme(nameAndCmd.Value()))
{
foundInvalidScheme = true;
break;
}
}
if (foundInvalidScheme)
{
_warnings.Append(SettingsLoadWarnings::InvalidColorSchemeInCmd);
}
}
bool CascadiaSettings::_HasInvalidColorScheme(const Model::Command& command)
{
bool invalid{ false };
if (command.HasNestedCommands())
{
for (const auto& nested : command.NestedCommands())
{
if (_HasInvalidColorScheme(nested.Value()))
{
invalid = true;
break;
}
}
}
else if (const auto& actionAndArgs = command.ActionAndArgs())
{
if (const auto& realArgs = actionAndArgs.Args().try_as<Model::SetColorSchemeArgs>())
{
auto cmdImpl{ winrt::get_self<Command>(command) };
// no need to validate iterable commands on color schemes
// they will be expanded to commands with a valid scheme name
if (cmdImpl->IterateOn() != ExpandCommandType::ColorSchemes &&
!_globals->ColorSchemes().HasKey(realArgs.SchemeName()))
{
invalid = true;
}
}
}
return invalid;
}
// Method Description:
// - Checks for the presence of the legacy "globals" key in the user's
// settings.json. If this key is present, then they've probably got a pre-0.11
// settings file that won't work as expected anymore. We should warn them
// about that.
// Arguments:
// - <none>
// Return Value:
// - <none>
// - Appends a SettingsLoadWarnings::LegacyGlobalsProperty to our list of warnings if
// we find any invalid background images.
void CascadiaSettings::_ValidateNoGlobalsKey()
{
// use isMember here. If you use [], you're actually injecting "globals": null.
if (_userSettings.isMember("globals"))
{
_warnings.Append(SettingsLoadWarnings::LegacyGlobalsProperty);
}
}
// Method Description
// - Replaces known tokens DEFAULT_PROFILE, PRODUCT and VERSION in the settings template
// with their expected values. DEFAULT_PROFILE is updated to match PowerShell Core's GUID
// if such a profile is detected. If it isn't, it'll be set to Windows PowerShell's GUID.
// Arguments:
// - settingsTemplate: a settings template
// Return value:
// - The new settings string.
std::string CascadiaSettings::_ApplyFirstRunChangesToSettingsTemplate(std::string_view settingsTemplate) const
{
// We're using replace_needle_in_haystack_inplace here, because it's more
// efficient to iteratively modify a single string in-place than it is to
// keep copying over the contents and modifying a copy (which
// replace_needle_in_haystack would do).
std::string finalSettings{ settingsTemplate };
std::wstring defaultProfileGuid{ DEFAULT_WINDOWS_POWERSHELL_GUID };
if (const auto psCoreProfileGuid{ _GetProfileGuidByName(hstring{ PowershellCoreProfileGenerator::GetPreferredPowershellProfileName() }) })
{
defaultProfileGuid = Utils::GuidToString(*psCoreProfileGuid);
}
til::replace_needle_in_haystack_inplace(finalSettings,
"%DEFAULT_PROFILE%",
til::u16u8(defaultProfileGuid));
til::replace_needle_in_haystack_inplace(finalSettings,
"%VERSION%",
til::u16u8(ApplicationVersion()));
til::replace_needle_in_haystack_inplace(finalSettings,
"%PRODUCT%",
til::u16u8(ApplicationDisplayName()));
til::replace_needle_in_haystack_inplace(finalSettings,
"%COMMAND_PROMPT_LOCALIZED_NAME%",
RS_A(L"CommandPromptDisplayName"));
return finalSettings;
}
// Method Description:
// - Lookup the color scheme for a given profile. If the profile doesn't exist,
// or the scheme name listed in the profile doesn't correspond to a scheme,
// this will return `nullptr`.
// Arguments:
// - profileGuid: the GUID of the profile to find the scheme for.
// Return Value:
// - a non-owning pointer to the scheme.
winrt::Microsoft::Terminal::Settings::Model::ColorScheme CascadiaSettings::GetColorSchemeForProfile(const winrt::guid profileGuid) const
{
auto profile = FindProfile(profileGuid);
if (!profile)
{
return nullptr;
}
const auto schemeName = profile.DefaultAppearance().ColorSchemeName();
return _globals->ColorSchemes().TryLookup(schemeName);
}
// Method Description:
// - updates all references to that color scheme with the new name
// Arguments:
// - oldName: the original name for the color scheme
// - newName: the new name for the color scheme
// Return Value:
// - <none>
void CascadiaSettings::UpdateColorSchemeReferences(const hstring oldName, const hstring newName)
{
// update profiles.defaults, if necessary
if (_userDefaultProfileSettings &&
_userDefaultProfileSettings->DefaultAppearance().HasColorSchemeName() &&
_userDefaultProfileSettings->DefaultAppearance().ColorSchemeName() == oldName)
{
_userDefaultProfileSettings->DefaultAppearance().ColorSchemeName(newName);
}
// update all profiles referencing this color scheme
for (const auto& profile : _allProfiles)
{
if (profile.DefaultAppearance().HasColorSchemeName() && profile.DefaultAppearance().ColorSchemeName() == oldName)
{
profile.DefaultAppearance().ColorSchemeName(newName);
}
if (profile.UnfocusedAppearance())
{
if (profile.UnfocusedAppearance().HasColorSchemeName() && profile.UnfocusedAppearance().ColorSchemeName() == oldName)
{
profile.UnfocusedAppearance().ColorSchemeName(newName);
}
}
}
}
winrt::hstring CascadiaSettings::ApplicationDisplayName()
{
try
{
const auto package{ winrt::Windows::ApplicationModel::Package::Current() };
return package.DisplayName();
}
CATCH_LOG();
return RS_(L"ApplicationDisplayNameUnpackaged");
}
winrt::hstring CascadiaSettings::ApplicationVersion()
{
try
{
const auto package{ winrt::Windows::ApplicationModel::Package::Current() };
const auto version{ package.Id().Version() };
winrt::hstring formatted{ wil::str_printf<std::wstring>(L"%u.%u.%u.%u", version.Major, version.Minor, version.Build, version.Revision) };
return formatted;
}
CATCH_LOG();
// Try to get the version the old-fashioned way
try
{
struct LocalizationInfo
{
WORD language, codepage;
};
// Use the current module instance handle for TerminalApp.dll, nullptr for WindowsTerminal.exe
auto filename{ wil::GetModuleFileNameW<std::wstring>(wil::GetModuleInstanceHandle()) };
auto size{ GetFileVersionInfoSizeExW(0, filename.c_str(), nullptr) };
THROW_LAST_ERROR_IF(size == 0);
auto versionBuffer{ std::make_unique<std::byte[]>(size) };
THROW_IF_WIN32_BOOL_FALSE(GetFileVersionInfoExW(0, filename.c_str(), 0, size, versionBuffer.get()));
// Get the list of Version localizations
LocalizationInfo* pVarLocalization{ nullptr };
UINT varLen{ 0 };
THROW_IF_WIN32_BOOL_FALSE(VerQueryValueW(versionBuffer.get(), L"\\VarFileInfo\\Translation", reinterpret_cast<void**>(&pVarLocalization), &varLen));
THROW_HR_IF(E_UNEXPECTED, varLen < sizeof(*pVarLocalization)); // there must be at least one translation
// Get the product version from the localized version compartment
// We're using String/ProductVersion here because our build pipeline puts more rich information in it (like the branch name)
// than in the unlocalized numeric version fields.
WCHAR* pProductVersion{ nullptr };
UINT versionLen{ 0 };
const auto localizedVersionName{ wil::str_printf<std::wstring>(L"\\StringFileInfo\\%04x%04x\\ProductVersion",
pVarLocalization->language ? pVarLocalization->language : 0x0409, // well-known en-US LCID
pVarLocalization->codepage) };
THROW_IF_WIN32_BOOL_FALSE(VerQueryValueW(versionBuffer.get(), localizedVersionName.c_str(), reinterpret_cast<void**>(&pProductVersion), &versionLen));
return { pProductVersion };
}
CATCH_LOG();
return RS_(L"ApplicationVersionUnknown");
}
// Method Description:
// - Forces a refresh of all default terminal state. This hits the registry to
// read off the disk, so best to not do it on the UI thread.
// Arguments:
// - <none>
// Return Value:
// - <none> - Updates internal state
void CascadiaSettings::RefreshDefaultTerminals()
{
_defaultTerminals.Clear();
for (const auto& term : Model::DefaultTerminal::Available())
{
_defaultTerminals.Append(term);
}
_currentDefaultTerminal = Model::DefaultTerminal::Current();
}
// Helper to do the version check
static bool _isOnBuildWithDefTerm() noexcept
{
OSVERSIONINFOEXW osver{ 0 };
osver.dwOSVersionInfoSize = sizeof(osver);
osver.dwBuildNumber = 21359;
DWORDLONG dwlConditionMask = 0;
VER_SET_CONDITION(dwlConditionMask, VER_BUILDNUMBER, VER_GREATER_EQUAL);
return VerifyVersionInfoW(&osver, VER_BUILDNUMBER, dwlConditionMask);
}
// Method Description:
// - Determines if we're on an OS platform that supports
// the default terminal handoff functionality.
// Arguments:
// - <none>
// Return Value:
// - True if OS supports default terminal. False otherwise.
bool CascadiaSettings::IsDefaultTerminalAvailable() noexcept
{
// Cached on first use since the OS version shouldn't change while we're running.
static bool isAvailable = _isOnBuildWithDefTerm();
return isAvailable;
}
// Method Description:
// - Returns an iterable collection of all available terminals.
// Arguments:
// - <none>
// Return Value:
// - an iterable collection of all available terminals that could be the default.
IObservableVector<Settings::Model::DefaultTerminal> CascadiaSettings::DefaultTerminals() const noexcept
{
return _defaultTerminals;
}
// Method Description:
// - Returns the currently selected default terminal application.
// - DANGER! This will be null unless you've called
// CascadiaSettings::RefreshDefaultTerminals. At the time of this comment (May
// 2021), only the Launch page in the settings UI calls that method, so this
// value is unset unless you've navigated to that page.
// Arguments:
// - <none>
// Return Value:
// - the selected default terminal application
Settings::Model::DefaultTerminal CascadiaSettings::CurrentDefaultTerminal() const noexcept
{
return _currentDefaultTerminal;
}
// Method Description:
// - Sets the current default terminal application
// Arguments:
// - terminal - Terminal from `DefaultTerminals` list to set as default
// Return Value:
// - <none>
void CascadiaSettings::CurrentDefaultTerminal(Settings::Model::DefaultTerminal terminal)
{
_currentDefaultTerminal = terminal;
}