terminal/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp
Leonard Hecker 32fbd4cbb6
Enable /Zc:preprocessor (#10593)
This commit is a preparation for upcoming changes to KeyChordSerialization for #7539 and #10203.
In order to support variadic macros, /Zc:preprocessor was enabled, which required changing unrelated parts of the project.

## PR Checklist
* [x] I work here
* [x] Tests added/passed

## Validation Steps Performed

* Project still compiles ✔️
2021-07-13 23:00:11 +00:00

1231 lines
46 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;
}
winrt::hstring newName{};
for (uint32_t candidateIndex = 0; candidateIndex < _allProfiles.Size() + 1; candidateIndex++)
{
// There is a theoretical unsigned integer wraparound, which is OK
newName = fmt::format(L"Profile {}", _allProfiles.Size() + 1 + candidateIndex);
if (std::none_of(begin(_allProfiles), end(_allProfiles), [&](auto&& profile) { return profile.Name() == newName; }))
{
break;
}
}
auto newProfile{ _userDefaultProfileSettings->CreateChild() };
newProfile->Name(newName);
_allProfiles.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);
winrt::com_ptr<Profile> duplicated;
if (_userDefaultProfileSettings)
{
duplicated = _userDefaultProfileSettings->CreateChild();
}
else
{
duplicated = winrt::make_self<Profile>();
}
_allProfiles.Append(*duplicated);
if (!source.Hidden())
{
_activeProfiles.Append(*duplicated);
}
winrt::hstring 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; candidateIndex < _allProfiles.Size() + 1; ++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);
}
duplicated->Name(winrt::hstring(newName));
const auto isProfilesDefaultsOrigin = [](const auto& profile) -> bool {
return profile && profile.Origin() != OriginTag::ProfilesDefaults;
};
const 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()); \
}
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);
}
// 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());
}
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;
}
// 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;
}