terminal/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp
Leonard Hecker 168d28b036
Reduce usage of Json::Value throughout Terminal.Settings.Model (#11184)
This commit reduces the code surface that interacts with raw JSON data,
reducing code complexity and improving maintainability.
Files that needed to be changed drastically were additionally
cleaned up to remove any code cruft that has accrued over time.

In order to facility this the following changes were made:
* Move JSON handling from `CascadiaSettings` into `SettingsLoader`
  This allows us to use STL containers for data model instances.
  For instance profiles are now added to a hashmap for O(1) lookup.
* JSON parsing within `SettingsLoader` doesn't differentiate between user,
  inbox and fragment JSON data, reducing code complexity and size.
  It also centralizes common concerns, like profile deduplication and
  ensuring that all profiles are assigned a GUID.
* Direct JSON modification, like the insertion of dynamic profiles into
  settings.json were removed. This vastly reduces code complexity,
  but unfortunately removes support for comments in JSON on first start.
* `ColorScheme`s cannot be layered. As such its `LayerJson` API was replaced
  with `FromJson`, allowing us to remove JSON-based color scheme validation.
* `Profile`s used to test their wish to layer using `ShouldBeLayered`, which
  was replaced with a GUID-based hashmap lookup on previously parsed profiles.

Further changes were made as improvements upon the previous changes:
* Compact the JSON files embedded binary, saving 28kB
* Prevent double-initialization of the color table in `ColorScheme`
* Making `til::color` getters `constexpr`, allow better optimizations

The result is a reduction of:
* 48kB binary size for the Settings.Model.dll
* 5-10% startup duration
* 26% code for the `CascadiaSettings` class
* 1% overall code in this project

Furthermore this results in the following breaking changes:
* The long deprecated "globals" settings object will not be detected and no
  warning will be created during load.
* The initial creation of a new settings.json will not produce helpful comments.

Both cases are caused by the removal of manual JSON handling and the
move to representing the settings file with model objects instead

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

## Validation Steps Performed

* Out-of-box-experience is identical to before ✔️
  (Except for the settings.json file lacking comments.)
* Existing user settings load correctly ✔️
* New WSL instances are added to user settings ✔️
* New fragments are added to user settings ✔️
* All profiles are assigned GUIDs ✔️
2021-09-22 16:27:31 +00:00

897 lines
35 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "CascadiaSettings.h"
#include <LibraryResources.h>
#include <fmt/chrono.h>
#include <shlobj.h>
#include <til/latch.h>
#include "AzureCloudShellGenerator.h"
#include "PowershellCoreProfileGenerator.h"
#include "VsDevCmdGenerator.h"
#include "VsDevShellGenerator.h"
#include "WslDistroGenerator.h"
// The following files are generated at build time into the "Generated Files" directory.
// defaults(-universal).h is a file containing the default json settings in a std::string_view.
#include "defaults.h"
#include "defaults-universal.h"
// userDefault.h is like the above, but with a default template for the user's settings.json.
#include <LegacyProfileGeneratorNamespaces.h>
#include "userDefaults.h"
#include "ApplicationState.h"
#include "FileUtils.h"
using namespace winrt::Microsoft::Terminal::Settings;
using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
static constexpr std::wstring_view SettingsFilename{ L"settings.json" };
static constexpr std::wstring_view DefaultsFilename{ L"defaults.json" };
static constexpr std::string_view ProfilesKey{ "profiles" };
static constexpr std::string_view DefaultSettingsKey{ "defaults" };
static constexpr std::string_view ProfilesListKey{ "list" };
static constexpr std::string_view SchemesKey{ "schemes" };
static constexpr std::string_view NameKey{ "name" };
static constexpr std::string_view GuidKey{ "guid" };
static constexpr std::wstring_view jsonExtension{ L".json" };
static constexpr std::wstring_view FragmentsSubDirectory{ L"\\Fragments" };
static constexpr std::wstring_view FragmentsPath{ L"\\Microsoft\\Windows Terminal\\Fragments" };
static constexpr std::wstring_view AppExtensionHostName{ L"com.microsoft.windows.terminal.settings" };
// make sure this matches defaults.json.
static constexpr winrt::guid DEFAULT_WINDOWS_POWERSHELL_GUID{ 0x61c54bbd, 0xc2c6, 0x5271, { 0x96, 0xe7, 0x00, 0x9a, 0x87, 0xff, 0x44, 0xbf } };
static constexpr winrt::guid DEFAULT_COMMAND_PROMPT_GUID{ 0x0caa0dad, 0x35be, 0x5f56, { 0xa8, 0xff, 0xaf, 0xce, 0xee, 0xaa, 0x61, 0x01 } };
// Function Description:
// - Extracting the value from an async task (like talking to the app catalog) when we are on the
// UI thread causes C++/WinRT to complain quite loudly (and halt execution!)
// This templated function extracts the result from a task with chicanery.
template<typename TTask>
static auto extractValueFromTaskWithoutMainThreadAwait(TTask&& task) -> decltype(task.get())
{
std::optional<decltype(task.get())> finalVal;
til::latch latch{ 1 };
const auto _ = [&]() -> winrt::fire_and_forget {
co_await winrt::resume_background();
finalVal.emplace(co_await task);
latch.count_down();
}();
latch.wait();
return finalVal.value();
}
// Concatenates the two given strings (!) and returns them as a path.
// You better make sure there's a path separator at the end of lhs or at the start of rhs.
static std::filesystem::path buildPath(const std::wstring_view& lhs, const std::wstring_view& rhs)
{
std::wstring buffer;
buffer.reserve(lhs.size() + rhs.size());
buffer.append(lhs);
buffer.append(rhs);
return { std::move(buffer) };
}
// This is a convenience method used by the CascadiaSettings constructor.
// It runs some basic settings layering without relying on external programs or files.
// This makes it suitable for most unit tests.
SettingsLoader SettingsLoader::Default(const std::string_view& userJSON, const std::string_view& inboxJSON)
{
SettingsLoader loader{ userJSON, inboxJSON };
loader.MergeInboxIntoUserSettings();
loader.FinalizeLayering();
return loader;
}
// The SettingsLoader class is an internal implementation detail of CascadiaSettings.
// Member methods aren't safe against misuse and you need to ensure to call them in a specific order.
// See CascadiaSettings::LoadAll() for a specific usage example.
//
// This constructor only handles parsing the two given JSON strings.
// At a minimum you should do at least everything that SettingsLoader::Default does.
SettingsLoader::SettingsLoader(const std::string_view& userJSON, const std::string_view& inboxJSON)
{
_parse(OriginTag::InBox, {}, inboxJSON, inboxSettings);
try
{
_parse(OriginTag::User, {}, userJSON, userSettings);
}
catch (const JsonUtils::DeserializationError& e)
{
_rethrowSerializationExceptionWithLocationInfo(e, userJSON);
}
if (const auto sources = userSettings.globals->DisabledProfileSources())
{
_ignoredNamespaces.reserve(sources.Size());
for (const auto& id : sources)
{
_ignoredNamespaces.emplace(id);
}
}
// See member description of _userProfileCount.
_userProfileCount = userSettings.profiles.size();
}
// Generate dynamic profiles and add them to the list of "inbox" profiles
// (meaning profiles specified by the application rather by the user).
void SettingsLoader::GenerateProfiles()
{
_executeGenerator(PowershellCoreProfileGenerator{});
_executeGenerator(WslDistroGenerator{});
_executeGenerator(AzureCloudShellGenerator{});
_executeGenerator(VsDevCmdGenerator{});
_executeGenerator(VsDevShellGenerator{});
}
// A new settings.json gets a special treatment:
// 1. The default profile is a PowerShell 7+ one, if one was generated,
// and falls back to the standard PowerShell 5 profile otherwise.
// 2. cmd.exe gets a localized name.
void SettingsLoader::ApplyRuntimeInitialSettings()
{
// 1.
{
const auto preferredPowershellProfile = PowershellCoreProfileGenerator::GetPreferredPowershellProfileName();
auto guid = DEFAULT_WINDOWS_POWERSHELL_GUID;
for (const auto& profile : inboxSettings.profiles)
{
if (profile->Name() == preferredPowershellProfile)
{
guid = profile->Guid();
break;
}
}
userSettings.globals->DefaultProfile(guid);
}
// 2.
{
for (const auto& profile : userSettings.profiles)
{
if (profile->Guid() == DEFAULT_COMMAND_PROMPT_GUID)
{
profile->Name(RS_(L"CommandPromptDisplayName"));
break;
}
}
}
}
// Adds profiles from .inboxSettings as parents of matching profiles in .userSettings.
// That way the user profiles will get appropriate defaults from the generators (like icons and such).
// If a matching profile doesn't exist yet in .userSettings, one will be created.
void SettingsLoader::MergeInboxIntoUserSettings()
{
for (const auto& profile : inboxSettings.profiles)
{
if (const auto [it, inserted] = userSettings.profilesByGuid.emplace(profile->Guid(), profile); !inserted)
{
// If inserted is false, we got a matching user profile with identical GUID.
// --> The generated profile is a parent of the existing user profile.
it->second->InsertParent(profile);
}
else
{
// If inserted is true, then this is a generated profile that doesn't exist in the user's settings.
// While emplace() has already created an appropriate entry in .profilesByGuid, we still need to
// add it to .profiles (which is basically a sorted list of .profilesByGuid's values).
//
// When a user modifies a profile they shouldn't modify the (static/constant)
// inbox profile of course. That's why we need to call CreateChild here.
userSettings.profiles.emplace_back(CreateChild(profile));
}
}
}
// Searches AppData/ProgramData and app extension directories for settings JSON files.
// If such JSON files are found, they're read and their contents added to .userSettings.
//
// Of course it would be more elegant to add fragments to .inboxSettings first and then have MergeInboxIntoUserSettings
// merge them. Unfortunately however the "updates" key in fragment profiles make this impossible:
// The targeted profile might be one that got created as part of SettingsLoader::MergeInboxIntoUserSettings.
// Additionally the GUID in "updates" will conflict with existing GUIDs in .inboxSettings.
void SettingsLoader::FindFragmentsAndMergeIntoUserSettings()
{
ParsedSettings fragmentSettings;
const auto parseAndLayerFragmentFiles = [&](const std::filesystem::path& path, const winrt::hstring& source) {
for (const auto& fragmentExt : std::filesystem::directory_iterator{ path })
{
if (fragmentExt.path().extension() == jsonExtension)
{
try
{
const auto content = ReadUTF8File(fragmentExt.path());
_parse(OriginTag::Fragment, source, content, fragmentSettings);
for (const auto& fragmentProfile : fragmentSettings.profiles)
{
if (const auto updates = fragmentProfile->Updates(); updates != winrt::guid{})
{
if (const auto it = userSettings.profilesByGuid.find(updates); it != userSettings.profilesByGuid.end())
{
it->second->InsertParent(0, fragmentProfile);
}
}
else
{
_appendProfile(CreateChild(fragmentProfile), userSettings);
}
}
for (const auto& kv : fragmentSettings.globals->ColorSchemes())
{
userSettings.globals->AddColorScheme(kv.Value());
}
}
CATCH_LOG();
}
}
};
for (const auto& rfid : std::array{ FOLDERID_LocalAppData, FOLDERID_ProgramData })
{
wil::unique_cotaskmem_string folder;
THROW_IF_FAILED(SHGetKnownFolderPath(rfid, 0, nullptr, &folder));
const auto fragmentPath = buildPath(folder.get(), FragmentsPath);
if (std::filesystem::is_directory(fragmentPath))
{
for (const auto& fragmentExtFolder : std::filesystem::directory_iterator{ fragmentPath })
{
const auto filename = fragmentExtFolder.path().filename();
const auto& source = filename.native();
if (!_ignoredNamespaces.count(std::wstring_view{ source }) && fragmentExtFolder.is_directory())
{
parseAndLayerFragmentFiles(fragmentExtFolder.path(), winrt::hstring{ source });
}
}
}
}
// Search through app extensions
// Gets the catalog of extensions with the name "com.microsoft.windows.terminal.settings"
const auto catalog = winrt::Windows::ApplicationModel::AppExtensions::AppExtensionCatalog::Open(AppExtensionHostName);
const auto extensions = extractValueFromTaskWithoutMainThreadAwait(catalog.FindAllAsync());
for (const auto& ext : extensions)
{
const auto packageName = ext.Package().Id().FamilyName();
if (_ignoredNamespaces.count(std::wstring_view{ packageName }))
{
continue;
}
// Likewise, getting the public folder from an extension is an async operation.
auto foundFolder = extractValueFromTaskWithoutMainThreadAwait(ext.GetPublicFolderAsync());
if (!foundFolder)
{
continue;
}
// the StorageFolder class has its own methods for obtaining the files within the folder
// however, all those methods are Async methods
// you may have noticed that we need to resort to clunky implementations for async operations
// (they are in extractValueFromTaskWithoutMainThreadAwait)
// so for now we will just take the folder path and access the files that way
const auto path = buildPath(foundFolder.Path(), FragmentsSubDirectory);
if (std::filesystem::is_directory(path))
{
parseAndLayerFragmentFiles(path, packageName);
}
}
}
// Call this method before passing SettingsLoader to the CascadiaSettings constructor.
// It layers all remaining objects onto each other (those that aren't covered
// by MergeInboxIntoUserSettings/FindFragmentsAndMergeIntoUserSettings).
void SettingsLoader::FinalizeLayering()
{
// Layer default globals -> user globals
userSettings.globals->InsertParent(inboxSettings.globals);
userSettings.globals->_FinalizeInheritance();
// Layer default profile defaults -> user profile defaults
userSettings.baseLayerProfile->InsertParent(inboxSettings.baseLayerProfile);
userSettings.baseLayerProfile->_FinalizeInheritance();
// Layer user profile defaults -> user profiles
for (const auto& profile : userSettings.profiles)
{
profile->InsertParent(0, userSettings.baseLayerProfile);
profile->_FinalizeInheritance();
}
}
// Let's say a user doesn't know that they need to write `"hidden": true` in
// order to prevent a profile from showing up (and a settings UI doesn't exist).
// Naturally they would open settings.json and try to remove the profile object.
// This section of code recognizes if a profile was seen before and marks it as
// `"hidden": true` by default and thus ensures the behavior the user expects:
// Profiles won't show up again after they've been removed from settings.json.
bool SettingsLoader::DisableDeletedProfiles()
{
const auto& state = winrt::get_self<ApplicationState>(ApplicationState::SharedInstance());
auto generatedProfileIds = state->GeneratedProfiles();
bool newGeneratedProfiles = false;
for (const auto& profile : _getNonUserOriginProfiles())
{
if (generatedProfileIds.emplace(profile->Guid()).second)
{
newGeneratedProfiles = true;
}
else
{
profile->Deleted(true);
profile->Hidden(true);
}
}
if (newGeneratedProfiles)
{
state->GeneratedProfiles(generatedProfileIds);
}
return newGeneratedProfiles;
}
// Give a string of length N and a position of [0,N) this function returns
// the line/column within the string, similar to how text editors do it.
// Newlines are considered part of the current line (as per POSIX).
std::pair<size_t, size_t> SettingsLoader::_lineAndColumnFromPosition(const std::string_view& string, const size_t position)
{
size_t line = 1;
size_t column = 0;
for (;;)
{
const auto p = string.find('\n', column);
if (p >= position)
{
break;
}
column = p + 1;
line++;
}
return { line, position - column + 1 };
}
// Formats a JSON exception for humans to read and throws that.
void SettingsLoader::_rethrowSerializationExceptionWithLocationInfo(const JsonUtils::DeserializationError& e, const std::string_view& settingsString)
{
std::string jsonValueAsString;
try
{
jsonValueAsString = e.jsonValue.asString();
if (e.jsonValue.isString())
{
jsonValueAsString = fmt::format("\"{}\"", jsonValueAsString);
}
}
catch (...)
{
jsonValueAsString = "array or object";
}
const auto [line, column] = _lineAndColumnFromPosition(settingsString, static_cast<size_t>(e.jsonValue.getOffsetStart()));
fmt::memory_buffer msg;
fmt::format_to(msg, "* Line {}, Column {}", line, column);
if (e.key)
{
fmt::format_to(msg, " ({})", *e.key);
}
fmt::format_to(msg, "\n Have: {}\n Expected: {}\0", jsonValueAsString, e.expectedType);
throw SettingsTypedDeserializationException{ msg.data() };
}
// Simply parses the given content to a Json::Value.
Json::Value SettingsLoader::_parseJSON(const std::string_view& content)
{
Json::Value json;
std::string errs;
const std::unique_ptr<Json::CharReader> reader{ Json::CharReaderBuilder::CharReaderBuilder().newCharReader() };
if (!reader->parse(content.data(), content.data() + content.size(), &json, &errs))
{
throw winrt::hresult_error(WEB_E_INVALID_JSON_STRING, winrt::to_hstring(errs));
}
return json;
}
// A helper method similar to Json::Value::operator[], but compatible with std::string_view.
const Json::Value& SettingsLoader::_getJSONValue(const Json::Value& json, const std::string_view& key) noexcept
{
if (json.isObject())
{
if (const auto val = json.find(key.data(), key.data() + key.size()))
{
return *val;
}
}
return Json::Value::nullSingleton();
}
// Returns true if the given Json::Value looks like a profile.
// We introduced a bug (GH#9962, fixed in GH#9964) that would result in one or
// more nameless, guid-less profiles being emitted into the user's settings file.
// Those profiles would show up in the list as "Default" later.
bool SettingsLoader::_isValidProfileObject(const Json::Value& profileJson)
{
return profileJson.isObject() &&
(profileJson.isMember(NameKey.data(), NameKey.data() + NameKey.size()) || // has a name (can generate a guid)
profileJson.isMember(GuidKey.data(), GuidKey.data() + GuidKey.size())); // or has a guid
}
// We treat userSettings.profiles as an append-only array and will
// append profiles into the userSettings as necessary in this function.
// _userProfileCount stores the number of profiles that were in userJSON during construction.
//
// Thus no matter how many profiles are added later on, the following condition holds true:
// The userSettings.profiles in the range [0, _userProfileCount) contain all profiles specified by the user.
// In turn all profiles in the range [_userProfileCount, ∞) contain newly generated/added profiles.
// gsl::make_span(userSettings.profiles).subspan(_userProfileCount) gets us the latter range.
gsl::span<const winrt::com_ptr<Profile>> SettingsLoader::_getNonUserOriginProfiles() const
{
return gsl::make_span(userSettings.profiles).subspan(_userProfileCount);
}
// Parses the given JSON string ("content") and fills a ParsedSettings instance with it.
void SettingsLoader::_parse(const OriginTag origin, const winrt::hstring& source, const std::string_view& content, ParsedSettings& settings)
{
const auto json = content.empty() ? Json::Value{ Json::ValueType::objectValue } : _parseJSON(content);
const auto& profilesObject = _getJSONValue(json, ProfilesKey);
const auto& defaultsObject = _getJSONValue(profilesObject, DefaultSettingsKey);
const auto& profilesArray = profilesObject.isArray() ? profilesObject : _getJSONValue(profilesObject, ProfilesListKey);
// globals
{
settings.globals = GlobalAppSettings::FromJson(json);
if (const auto& schemes = _getJSONValue(json, SchemesKey))
{
for (const auto& schemeJson : schemes)
{
if (schemeJson.isObject())
{
if (const auto scheme = ColorScheme::FromJson(schemeJson))
{
settings.globals->AddColorScheme(*scheme);
}
}
}
}
}
// profiles.defaults
{
settings.baseLayerProfile = Profile::FromJson(defaultsObject);
// Remove the `guid` member from the default settings.
// That will hyper-explode, so just don't let them do that.
settings.baseLayerProfile->ClearGuid();
settings.baseLayerProfile->Origin(OriginTag::ProfilesDefaults);
}
// profiles.list
{
const auto size = profilesArray.size();
// NOTE: This function is supposed to *replace* the contents of ParsedSettings. Don't break this promise.
// SettingsLoader::FindFragmentsAndMergeIntoUserSettings relies on this.
settings.profiles.clear();
settings.profiles.reserve(size);
settings.profilesByGuid.clear();
settings.profilesByGuid.reserve(size);
for (const auto& profileJson : profilesArray)
{
if (_isValidProfileObject(profileJson))
{
auto profile = Profile::FromJson(profileJson);
profile->Origin(origin);
// The Guid() generation below depends on the value of Source().
// --> Provide one if we got one.
if (!source.empty())
{
profile->Source(source);
}
// The Guid() getter generates one from Name() and Source() if none exists otherwise.
// We want to ensure that every profile has a GUID no matter what, not just to
// cache the value, but also to make them consistently identifiable later on.
if (!profile->HasGuid())
{
profile->Guid(profile->Guid());
}
_appendProfile(std::move(profile), settings);
}
}
}
}
// Adds a profile to the ParsedSettings instance. Takes ownership of the profile.
// It ensures no duplicate GUIDs are added to the ParsedSettings instance.
void SettingsLoader::_appendProfile(winrt::com_ptr<Profile>&& profile, ParsedSettings& settings)
{
// FYI: The static_cast ensures we don't move the profile into
// `profilesByGuid`, even though we still need it later for `profiles`.
if (settings.profilesByGuid.emplace(profile->Guid(), static_cast<const winrt::com_ptr<Profile>&>(profile)).second)
{
settings.profiles.emplace_back(profile);
}
else
{
duplicateProfile = true;
}
}
// As the name implies it executes a generator.
// Generated profiles are added to .inboxSettings. Used by GenerateProfiles().
void SettingsLoader::_executeGenerator(const IDynamicProfileGenerator& generator)
{
const auto generatorNamespace = generator.GetNamespace();
if (_ignoredNamespaces.count(generatorNamespace))
{
return;
}
const auto previousSize = inboxSettings.profiles.size();
try
{
generator.GenerateProfiles(inboxSettings.profiles);
}
CATCH_LOG_MSG("Dynamic Profile Namespace: \"%.*s\"", gsl::narrow<int>(generatorNamespace.size()), generatorNamespace.data())
// If the generator produced some profiles we're going to give them default attributes.
// By setting the Origin/Source/etc. here, we deduplicate some code and ensure they aren't missing accidentally.
if (inboxSettings.profiles.size() > previousSize)
{
const winrt::hstring source{ generatorNamespace };
for (const auto& profile : gsl::span(inboxSettings.profiles).subspan(previousSize))
{
profile->Origin(OriginTag::Generated);
profile->Source(source);
}
}
}
// Method Description:
// - Creates a CascadiaSettings from whatever's saved on disk, or instantiates
// a new one with the default values. If we're running as a packaged app,
// it will load the settings from our packaged localappdata. If we're
// running as an unpackaged application, it will read it from the path
// we've set under localappdata.
// - Loads both the settings from the defaults.json and the user's settings.json
// - Also runs and dynamic profile generators. If any of those generators create
// new profiles, we'll write the user settings back to the file, with the new
// profiles inserted into their list of profiles.
// Return Value:
// - a unique_ptr containing a new CascadiaSettings object.
Model::CascadiaSettings CascadiaSettings::LoadAll()
try
{
const auto settingsString = ReadUTF8FileIfExists(_settingsPath()).value_or(std::string{});
const auto firstTimeSetup = settingsString.empty();
const auto settingsStringView = firstTimeSetup ? UserSettingsJson : settingsString;
auto mustWriteToDisk = firstTimeSetup;
SettingsLoader loader{ settingsStringView, DefaultJson };
// Generate dynamic profiles and add them as parents of user profiles.
// That way the user profiles will get appropriate defaults from the generators (like icons and such).
loader.GenerateProfiles();
// ApplyRuntimeInitialSettings depends on generated profiles.
// --> ApplyRuntimeInitialSettings must be called after GenerateProfiles.
if (firstTimeSetup)
{
loader.ApplyRuntimeInitialSettings();
}
loader.MergeInboxIntoUserSettings();
// Fragments might reference user profiles created by a generator.
// --> FindFragmentsAndMergeIntoUserSettings must be called after MergeInboxIntoUserSettings.
loader.FindFragmentsAndMergeIntoUserSettings();
loader.FinalizeLayering();
// DisableDeletedProfiles returns true whenever we encountered any new generated/dynamic profiles.
// Coincidentally this is also the time we should write the new settings.json
// to disk (so that it contains the new profiles for manual editing by the user).
mustWriteToDisk |= loader.DisableDeletedProfiles();
// If this throws, the app will catch it and use the default settings.
const auto settings = winrt::make_self<CascadiaSettings>(std::move(loader));
// If we created the file, or found new dynamic profiles, write the user
// settings string back to the file.
if (mustWriteToDisk)
{
try
{
settings->WriteSettingsToDisk();
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
settings->_warnings.Append(SettingsLoadWarnings::FailedToWriteToSettings);
}
}
return *settings;
}
catch (const SettingsException& ex)
{
const auto settings{ winrt::make_self<CascadiaSettings>() };
settings->_loadError = ex.Error();
return *settings;
}
catch (const SettingsTypedDeserializationException& e)
{
const auto settings{ winrt::make_self<CascadiaSettings>() };
settings->_deserializationErrorMessage = til::u8u16(e.what());
return *settings;
}
// Function Description:
// - Loads a batch of settings curated for the Universal variant of the terminal app
// Arguments:
// - <none>
// Return Value:
// - a unique_ptr to a CascadiaSettings with the connection types and settings for Universal terminal
Model::CascadiaSettings CascadiaSettings::LoadUniversal()
{
return *winrt::make_self<CascadiaSettings>(std::string_view{}, DefaultUniversalJson);
}
// Function Description:
// - Creates a new CascadiaSettings object initialized with settings from the
// hardcoded defaults.json.
// Arguments:
// - <none>
// Return Value:
// - a unique_ptr to a CascadiaSettings with the settings from defaults.json
Model::CascadiaSettings CascadiaSettings::LoadDefaults()
{
return *winrt::make_self<CascadiaSettings>(std::string_view{}, DefaultJson);
}
CascadiaSettings::CascadiaSettings(const winrt::hstring& userJSON, const winrt::hstring& inboxJSON) :
CascadiaSettings{ SettingsLoader::Default(til::u16u8(userJSON), til::u16u8(inboxJSON)) }
{
}
CascadiaSettings::CascadiaSettings(const std::string_view& userJSON, const std::string_view& inboxJSON) :
CascadiaSettings{ SettingsLoader::Default(userJSON, inboxJSON) }
{
}
CascadiaSettings::CascadiaSettings(SettingsLoader&& loader)
{
std::vector<Model::Profile> allProfiles;
std::vector<Model::Profile> activeProfiles;
std::vector<Model::SettingsLoadWarnings> warnings;
allProfiles.reserve(loader.userSettings.profiles.size());
activeProfiles.reserve(loader.userSettings.profiles.size());
for (const auto& profile : loader.userSettings.profiles)
{
// If a generator stops producing a certain profile (e.g. WSL or PowerShell were removed) or
// a profile from a fragment doesn't exist anymore, we should also stop including the
// matching user's profile in _allProfiles (since they aren't functional anyways).
//
// A user profile has a valid, dynamic parent if it has a parent with identical source.
if (const auto source = profile->Source(); !source.empty())
{
const auto& parents = profile->Parents();
if (std::none_of(parents.begin(), parents.end(), [&](const auto& parent) { return parent->Source() == source; }))
{
continue;
}
}
allProfiles.emplace_back(*profile);
if (!profile->Hidden())
{
activeProfiles.emplace_back(*profile);
}
}
if (allProfiles.empty())
{
throw SettingsException(SettingsLoadErrors::NoProfiles);
}
if (activeProfiles.empty())
{
throw SettingsException(SettingsLoadErrors::AllProfilesHidden);
}
if (loader.duplicateProfile)
{
warnings.emplace_back(Model::SettingsLoadWarnings::DuplicateProfile);
}
// SettingsLoader and ParsedSettings are supposed to always
// create these two members. We don't want null-pointer exceptions.
assert(loader.userSettings.globals != nullptr);
assert(loader.userSettings.baseLayerProfile != nullptr);
_globals = loader.userSettings.globals;
_baseLayerProfile = loader.userSettings.baseLayerProfile;
_allProfiles = winrt::single_threaded_observable_vector(std::move(allProfiles));
_activeProfiles = winrt::single_threaded_observable_vector(std::move(activeProfiles));
_warnings = winrt::single_threaded_vector(std::move(warnings));
_resolveDefaultProfile();
_validateSettings();
}
// Method Description:
// - Returns the path of the settings.json file.
// Arguments:
// - <none>
// Return Value:
// - Returns a path in 80% of cases. I measured!
const std::filesystem::path& CascadiaSettings::_settingsPath()
{
static const auto path = GetBaseSettingsPath() / SettingsFilename;
return path;
}
// function Description:
// - Returns the full path to the settings file, either within the application
// package, or in its unpackaged location. This path is under the "Local
// AppData" folder, so it _doesn't_ roam to other machines.
// - If the application is unpackaged,
// the file will end up under e.g. C:\Users\admin\AppData\Local\Microsoft\Windows Terminal\settings.json
// Arguments:
// - <none>
// Return Value:
// - the full path to the settings file
winrt::hstring CascadiaSettings::SettingsPath()
{
return winrt::hstring{ _settingsPath().native() };
}
winrt::hstring CascadiaSettings::DefaultSettingsPath()
{
// Both of these posts suggest getting the path to the exe, then removing
// the exe's name to get the package root:
// * https://blogs.msdn.microsoft.com/appconsult/2017/06/23/accessing-to-the-files-in-the-installation-folder-in-a-desktop-bridge-application/
// * https://blogs.msdn.microsoft.com/appconsult/2017/03/06/handling-data-in-a-converted-desktop-app-with-the-desktop-bridge/
//
// This would break if we ever moved our exe out of the package root.
// HOWEVER, if we try to look for a defaults.json that's simply in the same
// directory as the exe, that will work for unpackaged scenarios as well. So
// let's try that.
const auto exePathString = wil::GetModuleFileNameW<std::wstring>(nullptr);
std::filesystem::path path{ exePathString };
path.replace_filename(DefaultsFilename);
return winrt::hstring{ path.native() };
}
// Method Description:
// - Write the current state of CascadiaSettings to our settings file
// - Create a backup file with the current contents, if one does not exist
// - Persists the default terminal handler choice to the registry
// Arguments:
// - <none>
// Return Value:
// - <none>
void CascadiaSettings::WriteSettingsToDisk() const
{
const auto settingsPath = _settingsPath();
{
// create a timestamped backup file
const auto backupSettingsPath = fmt::format(L"{}.{:%Y-%m-%dT%H-%M-%S}.backup", settingsPath.native(), fmt::localtime(std::time(nullptr)));
LOG_IF_WIN32_BOOL_FALSE(CopyFileW(settingsPath.c_str(), backupSettingsPath.c_str(), TRUE));
}
// write current settings to current settings file
Json::StreamWriterBuilder wbuilder;
wbuilder.settings_["indentation"] = " ";
wbuilder.settings_["enableYAMLCompatibility"] = true; // suppress spaces around colons
const auto styledString{ Json::writeString(wbuilder, ToJson()) };
WriteUTF8FileAtomic(settingsPath, styledString);
// Persists the default terminal choice
// GH#10003 - Only do this if _currentDefaultTerminal was actually initialized.
if (_currentDefaultTerminal)
{
DefaultTerminal::Current(_currentDefaultTerminal);
}
}
// Method Description:
// - Create a new serialized JsonObject from an instance of this class
// Arguments:
// - <none>
// Return Value:
// the JsonObject representing this instance
Json::Value CascadiaSettings::ToJson() const
{
// top-level json object
Json::Value json{ _globals->ToJson() };
json["$help"] = "https://aka.ms/terminal-documentation";
json["$schema"] = "https://aka.ms/terminal-profiles-schema";
// "profiles" will always be serialized as an object
Json::Value profiles{ Json::ValueType::objectValue };
profiles[JsonKey(DefaultSettingsKey)] = _baseLayerProfile ? _baseLayerProfile->ToJson() : Json::ValueType::objectValue;
Json::Value profilesList{ Json::ValueType::arrayValue };
for (const auto& entry : _allProfiles)
{
if (!entry.Deleted())
{
const auto prof{ winrt::get_self<Profile>(entry) };
profilesList.append(prof->ToJson());
}
}
profiles[JsonKey(ProfilesListKey)] = profilesList;
json[JsonKey(ProfilesKey)] = profiles;
// TODO GH#8100:
// "schemes" will be an accumulation of _all_ the color schemes
// including all of the ones from defaults.json
Json::Value schemes{ Json::ValueType::arrayValue };
for (const auto& entry : _globals->ColorSchemes())
{
const auto scheme{ winrt::get_self<ColorScheme>(entry.Value()) };
schemes.append(scheme->ToJson());
}
json[JsonKey(SchemesKey)] = schemes;
return json;
}
// 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
{
if (const auto unparsedDefaultProfile = _globals->UnparsedDefaultProfile(); !unparsedDefaultProfile.empty())
{
if (const auto profile = GetProfileByName(unparsedDefaultProfile))
{
_globals->DefaultProfile(profile.Guid());
return;
}
_warnings.Append(SettingsLoadWarnings::MissingDefaultProfile);
}
// Use the first profile as the new default.
GlobalSettings().DefaultProfile(_allProfiles.GetAt(0).Guid());
}