terminal/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp
Carlos Zamora 1b2aad6504
Add SSH folder to NTM for dynamic SSH profiles (#19239)
Automatically generates an "SSH" folder in the new tab menu that
contains all profiles generated by the SSH profile generator. This
folder is created if the SSH generator created some profiles and the
folder hasn't been created before. Detecting if the folder was generated
is done via the new `bool ApplicationState::SSHFolderGenerated`. The
logic is similar to `SettingsLoader::DisableDeletedProfiles()`.

Found a bug on new tab menu's folder inlining feature where we were
counting the number of raw entries to determine whether to inline or
not. Since the folder only contained the match profiles entry, this bug
made it so that the profile entries would always be inlined. The fix was
very simple: count the number of _resolved_ entries instead of the raw
entries. This can be pulled into its own PR and serviced, if desired.

## References and Relevant Issues
#18814 
#14042 

## Validation Steps Performed
 Existing users get an SSH folder if profiles were generated

## PR Checklist
Closes #19043
2025-08-13 15:43:27 -07:00

1948 lines
80 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 <til/io.h>
#include "resource.h"
#include "AzureCloudShellGenerator.h"
#include "PowershellCoreProfileGenerator.h"
#include "VisualStudioGenerator.h"
#include "WslDistroGenerator.h"
#if TIL_FEATURE_DYNAMICSSHPROFILES_ENABLED
#include "SshHostGenerator.h"
#endif
#include "ApplicationState.h"
#include "DefaultTerminal.h"
#include "FileUtils.h"
#include "ProfileEntry.h"
#include "FolderEntry.h"
#include "MatchProfilesEntry.h"
#include "WtExeUtils.h"
using namespace winrt::Windows::Foundation::Collections;
using namespace winrt::Windows::ApplicationModel::AppExtensions;
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 ThemesKey{ "themes" };
constexpr std::wstring_view systemThemeName{ L"system" };
constexpr std::wstring_view darkThemeName{ L"dark" };
constexpr std::wstring_view lightThemeName{ L"light" };
constexpr std::wstring_view legacySystemThemeName{ L"legacySystem" };
constexpr std::wstring_view legacyDarkThemeName{ L"legacyDark" };
constexpr std::wstring_view legacyLightThemeName{ L"legacyLight" };
static constexpr std::array builtinThemes{
systemThemeName,
lightThemeName,
darkThemeName,
legacySystemThemeName,
legacyLightThemeName,
legacyDarkThemeName,
};
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 _ = [&]() -> safe_void_coroutine {
const auto cleanup = wil::scope_exit([&]() {
latch.count_down();
});
co_await winrt::resume_background();
finalVal.emplace(co_await task);
}();
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) };
}
void ParsedSettings::clear()
{
globals = {};
baseLayerProfile = {};
profiles.clear();
profilesByGuid.clear();
colorSchemes.clear();
fixupsAppliedDuringLoad = false;
themesChangeLog.clear();
}
// 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();
loader.FixupUserSettings();
return loader;
}
std::vector<Model::ExtensionPackage> SettingsLoader::LoadExtensionPackages()
{
SettingsLoader loader{};
loader.GenerateExtensionPackagesFromProfileGenerators();
loader.FindFragmentsAndMergeIntoUserSettings(true /*generateExtensionPackages*/);
std::vector<Model::ExtensionPackage> extensionPackages;
for (auto [_, extPkg] : loader.extensionPackageMap)
{
extensionPackages.emplace_back(std::move(*extPkg));
}
return extensionPackages;
}
// 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 (auto&& id : sources)
{
_ignoredNamespaces.emplace(std::move(id));
}
}
// Apply DisabledProfileSources policy setting. Pick whatever policy is set first.
// In most cases HKCU settings take precedence over HKLM settings, but the inverse is true for policies.
for (const auto key : { HKEY_LOCAL_MACHINE, HKEY_CURRENT_USER })
{
wchar_t buffer[512]; // "640K ought to be enough for anyone"
DWORD bufferSize = sizeof(buffer);
if (RegGetValueW(key, LR"(Software\Policies\Microsoft\Windows Terminal)", L"DisabledProfileSources", RRF_RT_REG_MULTI_SZ, nullptr, buffer, &bufferSize) == 0)
{
for (auto p = buffer; *p;)
{
const auto len = wcslen(p);
_ignoredNamespaces.emplace(p, gsl::narrow_cast<uint32_t>(len));
p += len + 1;
}
break;
}
}
// See member description of _userProfileCount.
_userProfileCount = userSettings.profiles.size();
}
// This method is used to generate the JSON writer used for writing json in a styled format.
// We use it a few times throughout the loader, so we lazy load it and cache it here.
Json::StreamWriterBuilder SettingsLoader::_getJsonStyledWriter()
{
static bool jsonWriterInitialized = false;
static Json::StreamWriterBuilder styledWriter;
if (!jsonWriterInitialized)
{
styledWriter["indentation"] = " ";
styledWriter["commentStyle"] = "All";
styledWriter.settings_["enableYAMLCompatibility"] = true; // suppress spaces around colons
styledWriter.settings_["precision"] = 6; // prevent values like 1.1000000000000001
jsonWriterInitialized = true;
}
return styledWriter;
}
// 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()
{
auto generateProfiles = [&](const IDynamicProfileGenerator& generator) {
if (!_ignoredNamespaces.contains(generator.GetNamespace()))
{
const auto oldProfileCount = inboxSettings.profiles.size();
_executeGenerator(generator, inboxSettings.profiles);
return oldProfileCount != inboxSettings.profiles.size();
}
return false;
};
// Generate profiles for each generator and add them to the inbox settings.
// Be sure to update the same list below.
generateProfiles(PowershellCoreProfileGenerator{});
generateProfiles(WslDistroGenerator{});
generateProfiles(AzureCloudShellGenerator{});
generateProfiles(VisualStudioGenerator{});
#if TIL_FEATURE_DYNAMICSSHPROFILES_ENABLED
sshProfilesGenerated = generateProfiles(SshHostGenerator{});
#endif
}
// Generate ExtensionPackage objects from the profile generators.
void SettingsLoader::GenerateExtensionPackagesFromProfileGenerators()
{
auto generateExtensionPackages = [&](const IDynamicProfileGenerator& generator) {
std::vector<winrt::com_ptr<implementation::Profile>> profilesList;
_executeGenerator(generator, profilesList);
// These are needed for the FragmentSettings object
std::vector<Model::FragmentProfileEntry> profileEntries;
Json::Value profilesListJson{ Json::ValueType::arrayValue };
for (const auto& profile : profilesList)
{
const auto profileJson = profile->ToJson();
profilesListJson.append(profileJson);
profileEntries.push_back(winrt::make<FragmentProfileEntry>(profile->Guid(), hstring{ til::u8u16(Json::writeString(_getJsonStyledWriter(), profileJson)) }));
}
// Manually construct the JSON for the FragmentSettings object
Json::Value json{ Json::ValueType::objectValue };
json[JsonKey(ProfilesKey)] = profilesListJson;
auto generatorExtension = winrt::make_self<FragmentSettings>(hstring{ generator.GetNamespace() }, hstring{ til::u8u16(Json::writeString(_getJsonStyledWriter(), json)) }, hstring{ L"settings.json" });
generatorExtension->NewProfiles(winrt::single_threaded_vector<Model::FragmentProfileEntry>(std::move(profileEntries)));
auto extPkg = _registerFragment(std::move(*generatorExtension), FragmentScope::Machine);
extPkg->DisplayName(hstring{ generator.GetDisplayName() });
extPkg->Icon(hstring{ generator.GetIcon() });
};
// Generate extension package objects for each generator.
// Be sure to update the same list above.
generateExtensionPackages(PowershellCoreProfileGenerator{});
generateExtensionPackages(WslDistroGenerator{});
generateExtensionPackages(AzureCloudShellGenerator{});
generateExtensionPackages(VisualStudioGenerator{});
#if TIL_FEATURE_DYNAMICSSHPROFILES_ENABLED
generateExtensionPackages(SshHostGenerator{});
#endif
}
// 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.
// Additionally, produces a final view of the color schemes from the inbox + user settings
void SettingsLoader::MergeInboxIntoUserSettings()
{
for (const auto& profile : inboxSettings.profiles)
{
_addUserProfileParent(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(bool generateExtensionPackages)
{
ParsedSettings fragmentSettings;
const auto parseAndLayerFragmentFiles = [&](const std::filesystem::path& path, const winrt::hstring& source, FragmentScope scope) {
const winrt::hstring sourceBasePath{ path.native() };
for (const auto& fragmentExt : std::filesystem::directory_iterator{ path })
{
const auto fragExtPath = fragmentExt.path();
if (fragExtPath.extension() == jsonExtension)
{
try
{
const auto content = til::io::read_file_as_utf8_string_if_exists(fragExtPath);
if (!content.empty())
{
_parseFragment(source,
sourceBasePath,
content,
fragmentSettings,
generateExtensionPackages ?
static_cast<std::optional<ParseFragmentMetadata>>(ParseFragmentMetadata{ fragExtPath.filename().wstring(), scope }) :
std::nullopt);
}
}
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 (fragmentExtFolder.is_directory())
{
parseAndLayerFragmentFiles(fragmentExtFolder.path(),
winrt::hstring{ source },
rfid == FOLDERID_LocalAppData ? FragmentScope::User : FragmentScope::Machine); // scope
}
}
}
}
// Search through app extensions.
// Gets the catalog of extensions with the name "com.microsoft.windows.terminal.settings".
//
// GH#12305: Open() can throw an 0x80070490 "Element not found.".
// It's unclear to me under which circumstances this happens as no one on the team
// was able to reproduce the user's issue, even if the application was run unpackaged.
// The error originates from `CallerIdentity::GetCallingProcessAppId` which returns E_NOT_SET.
// A comment can be found, reading:
// > Gets the "strong" AppId from the process token. This works for UWAs and Centennial apps,
// > strongly named processes where the AppId is stored securely in the process token. [...]
// > E_NOT_SET is returned for processes without strong AppIds.
IVectorView<AppExtension> extensions;
try
{
const auto catalog = AppExtensionCatalog::Open(AppExtensionHostName);
extensions = extractValueFromTaskWithoutMainThreadAwait(catalog.FindAllAsync());
}
CATCH_LOG();
if (!extensions)
{
return;
}
for (const auto& ext : extensions)
{
const auto& package = ext.Package();
const auto packageName = package.Id().FamilyName();
// If the extension was explicitly disabled, skip over it early to avoid the async API!
// NOTE: only do this if we're NOT generating extension packages. If we are, we need to get all the
// package metadata anyway to display in the settings UI later.
if (!generateExtensionPackages && _ignoredNamespaces.contains(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))
{
// MSIX does not support machine-wide scope
// See https://github.com/microsoft/winget-cli/discussions/1983
parseAndLayerFragmentFiles(path,
packageName,
FragmentScope::User);
if (generateExtensionPackages)
{
auto extPkg = extensionPackageMap[packageName];
extPkg->Icon(package.Logo().AbsoluteUri());
extPkg->DisplayName(package.DisplayName());
}
}
}
}
// See FindFragmentsAndMergeIntoUserSettings.
// This function does the same, but for a single given JSON blob and source
// and at the time of writing is used for unit tests only.
void SettingsLoader::MergeFragmentIntoUserSettings(const winrt::hstring& source, const winrt::hstring& basePath, const std::string_view& content)
{
ParsedSettings fragmentSettings;
_parseFragment(source, basePath, content, fragmentSettings, std::nullopt);
}
// 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()
{
for (const auto& colorScheme : inboxSettings.colorSchemes)
{
_addOrMergeUserColorScheme(colorScheme.second);
}
// Layer default globals -> user globals
userSettings.globals->AddLeastImportantParent(inboxSettings.globals);
// Actions are currently global, so if we want to conditionally light up a bunch of
// actions, this is the time to do it.
if (userSettings.globals->EnableColorSelection())
{
const auto json = _parseJson(LoadStringResource(IDR_ENABLE_COLOR_SELECTION));
const auto globals = GlobalAppSettings::FromJson(json.root, OriginTag::InBox);
userSettings.globals->AddLeastImportantParent(globals);
}
userSettings.globals->_FinalizeInheritance();
// Layer default profile defaults -> user profile defaults
userSettings.baseLayerProfile->AddLeastImportantParent(inboxSettings.baseLayerProfile);
userSettings.baseLayerProfile->_FinalizeInheritance();
// Layer user profile defaults -> user profiles
for (const auto& profile : userSettings.profiles)
{
profile->AddMostImportantParent(userSettings.baseLayerProfile);
// This completes the parenting process that was started in _addUserProfileParent().
profile->_FinalizeInheritance();
if (profile->Origin() == OriginTag::None)
{
// If you add more fields here, make sure to do the same in
// implementation::CreateChild().
profile->Origin(OriginTag::User);
profile->Name(profile->Name());
profile->Hidden(profile->Hidden());
}
}
}
// 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.
//
// Returns true if something got changed and
// the settings need to be saved to disk.
bool SettingsLoader::DisableDeletedProfiles()
{
const auto& state = winrt::get_self<ApplicationState>(ApplicationState::SharedInstance());
auto generatedProfileIds = state->GeneratedProfiles();
auto 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;
}
// Returns true if something got changed and
// the settings need to be saved to disk.
bool SettingsLoader::AddDynamicProfileFolders()
{
// Keep track of generated folders to avoid regenerating them
const auto state = get_self<ApplicationState>(ApplicationState::SharedInstance());
// If the SSH generator is enabled, try to create an "SSH" folder with all the generated profiles
if (sshProfilesGenerated && !state->SSHFolderGenerated())
{
SshHostGenerator sshGenerator;
auto matchProfilesEntry = make_self<implementation::MatchProfilesEntry>();
matchProfilesEntry->Source(hstring{ sshGenerator.GetNamespace() });
auto folderEntry = make_self<implementation::FolderEntry>();
folderEntry->Name(L"SSH");
folderEntry->Icon(MediaResource::FromString(hstring{ sshGenerator.GetIcon() }));
folderEntry->Inlining(FolderEntryInlining::Auto);
folderEntry->RawEntries(winrt::single_threaded_vector<Model::NewTabMenuEntry>({ *matchProfilesEntry }));
userSettings.globals->NewTabMenu().Append(folderEntry.as<Model::NewTabMenuEntry>());
state->SSHFolderGenerated(true);
return true;
}
return false;
}
bool winrt::Microsoft::Terminal::Settings::Model::implementation::SettingsLoader::RemapColorSchemeForProfile(const winrt::com_ptr<winrt::Microsoft::Terminal::Settings::Model::implementation::Profile>& profile)
{
bool modified{ false };
const IAppearanceConfig appearances[] = {
profile->DefaultAppearance(),
profile->UnfocusedAppearance()
};
for (auto&& appearance : appearances)
{
if (appearance)
{
if (auto schemeName{ appearance.LightColorSchemeName() }; !schemeName.empty())
{
if (auto found{ userSettings.colorSchemeRemappings.find(schemeName) }; found != userSettings.colorSchemeRemappings.end())
{
appearance.LightColorSchemeName(found->second);
modified = true;
}
}
if (auto schemeName{ appearance.DarkColorSchemeName() }; !schemeName.empty())
{
if (auto found{ userSettings.colorSchemeRemappings.find(schemeName) }; found != userSettings.colorSchemeRemappings.end())
{
appearance.DarkColorSchemeName(found->second);
modified = true;
}
}
}
}
return modified;
}
// Runs migrations and fixups on user settings.
// Returns true if something got changed and
// the settings need to be saved to disk.
bool SettingsLoader::FixupUserSettings()
{
struct CommandlinePatch
{
winrt::guid guid{};
std::wstring_view before;
std::wstring_view after;
};
static constexpr std::array commandlinePatches{
CommandlinePatch{ DEFAULT_COMMAND_PROMPT_GUID, L"cmd.exe", L"%SystemRoot%\\System32\\cmd.exe" },
CommandlinePatch{ DEFAULT_WINDOWS_POWERSHELL_GUID, L"powershell.exe", L"%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" },
};
static constexpr std::array iconsToClearFromVisualStudioProfiles{
std::wstring_view{ L"ms-appx:///ProfileIcons/{61c54bbd-c2c6-5271-96e7-009a87ff44bf}.png" },
std::wstring_view{ L"ms-appx:///ProfileIcons/{0caa0dad-35be-5f56-a8ff-afceeeaa6101}.png" },
};
auto fixedUp = userSettings.fixupsAppliedDuringLoad;
fixedUp = userSettings.globals->FixupsAppliedDuringLoad() || fixedUp;
fixedUp = RemapColorSchemeForProfile(userSettings.baseLayerProfile) || fixedUp;
for (const auto& profile : userSettings.profiles)
{
fixedUp = RemapColorSchemeForProfile(profile) || fixedUp;
if (profile->HasCommandline())
{
for (const auto& patch : commandlinePatches)
{
if (profile->Guid() == patch.guid && til::equals_insensitive_ascii(profile->Commandline(), patch.before))
{
profile->ClearCommandline();
// GH#12842:
// With the commandline field on the user profile gone, it's actually unknown what
// commandline it'll inherit, since a user profile can have multiple parents. We have to
// make sure we restore the correct commandline in case we don't inherit the expected one.
if (profile->Commandline() != patch.after)
{
profile->Commandline(winrt::hstring{ patch.after });
}
fixedUp = true;
break;
}
}
}
if (profile->HasIcon() && profile->HasSource() && profile->Source() == VisualStudioGenerator::Namespace)
{
for (auto&& icon : iconsToClearFromVisualStudioProfiles)
{
if (profile->Icon().Path() == icon)
{
profile->ClearIcon();
fixedUp = true;
break;
}
}
}
}
// Terminal 1.19: Migrate the global
// `compatibility.reloadEnvironmentVariables` to being a per-profile
// setting. If the user had it disabled in 1.18, then set the
// profiles.defaults value to false to match.
if (!userSettings.globals->LegacyReloadEnvironmentVariables())
{
// migrate the user's opt-out to the profiles.defaults
userSettings.baseLayerProfile->ReloadEnvironmentVariables(false);
fixedUp = true;
}
// Terminal 1.23: Migrate the global
// `experimental.input.forceVT` to being a per-profile setting.
if (userSettings.globals->LegacyForceVTInput())
{
// migrate the user's opt-out to the profiles.defaults
userSettings.baseLayerProfile->ForceVTInput(true);
fixedUp = true;
}
return fixedUp;
}
// 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(std::back_inserter(msg), "* Line {}, Column {}", line, column);
if (e.key)
{
fmt::format_to(std::back_inserter(msg), " ({})", *e.key);
}
fmt::format_to(std::back_inserter(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{}.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();
}
// 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.
// std::span{ userSettings.profiles }.subspan(_userProfileCount) gets us the latter range.
std::span<const winrt::com_ptr<Profile>> SettingsLoader::_getNonUserOriginProfiles() const
{
return std::span{ userSettings.profiles }.subspan(_userProfileCount);
}
// Parses the given JSON string ("content") and fills a ParsedSettings instance with it.
// This function is to be used for user settings files.
void SettingsLoader::_parse(const OriginTag origin, const winrt::hstring& source, const std::string_view& content, ParsedSettings& settings)
{
const auto json = _parseJson(content);
settings.clear();
{
settings.globals = GlobalAppSettings::FromJson(json.root, origin);
for (const auto& schemeJson : json.colorSchemes)
{
if (const auto scheme = ColorScheme::FromJson(schemeJson))
{
scheme->Origin(origin);
settings.colorSchemes.emplace(scheme->Name(), std::move(scheme));
}
}
}
{
for (const auto& themeJson : json.themes)
{
if (const auto theme = Theme::FromJson(themeJson))
{
const auto& name{ theme->Name() };
if (origin != OriginTag::InBox &&
(std::ranges::find(builtinThemes, name) != builtinThemes.end()))
{
// If the theme didn't come from the in-box themes, and its
// name was one of the reserved names, then just ignore it.
// Themes don't support layering - we don't want the user
// versions of these themes overriding the built-in ones.
continue;
}
if (origin != OriginTag::InBox)
{
static std::string themesContext{ "themes" };
theme->LogSettingChanges(settings.themesChangeLog, themesContext);
}
settings.globals->AddTheme(*theme);
}
}
}
{
settings.baseLayerProfile = Profile::FromJson(json.profileDefaults);
// 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);
}
{
const auto size = json.profilesList.size();
settings.profiles.reserve(size);
settings.profilesByGuid.reserve(size);
for (const auto& profileJson : json.profilesList)
{
auto profile = _parseProfile(origin, source, profileJson);
// GH#9962: Discard Guid-less, Name-less profiles.
if (profile->HasGuid())
{
_appendProfile(std::move(profile), profile->Guid(), settings);
}
}
}
}
// Just like _parse, but is to be used for fragment files, which don't support anything but color
// schemes and profiles. Additionally this function supports profiles which specify an "updates" key.
// - fragmentMeta: If set, construct and register FragmentSettings objects. Provides metadata necessary for doing so.
// Otherwise, completely skip over that extra work and apply parsed settings to the user settings, if allowed by disabledProfileSources ("_ignoredNamespaces").
void SettingsLoader::_parseFragment(const winrt::hstring& source, const winrt::hstring& sourceBasePath, const std::string_view& content, ParsedSettings& settings, const std::optional<ParseFragmentMetadata>& fragmentMeta)
{
auto json = _parseJson(content);
const bool buildFragmentSettings = fragmentMeta.has_value();
const bool applyToUserSettings = !buildFragmentSettings && !_ignoredNamespaces.contains(std::wstring_view{ source });
winrt::com_ptr<implementation::FragmentSettings> fragmentSettings = buildFragmentSettings ?
winrt::make_self<FragmentSettings>(source, hstring{ til::u8u16(Json::writeString(_getJsonStyledWriter(), json.root)) }, hstring{ fragmentMeta->jsonFilename }) :
nullptr;
settings.clear();
// Load GlobalAppSettings and ColorSchemes
{
std::vector<Model::FragmentColorSchemeEntry> fragmentColorSchemes;
for (const auto& schemeJson : json.colorSchemes)
{
try
{
if (const auto scheme = ColorScheme::FromJson(schemeJson))
{
scheme->Origin(OriginTag::Fragment);
if (buildFragmentSettings)
{
fragmentColorSchemes.emplace_back(winrt::make<FragmentColorSchemeEntry>(scheme->Name(), hstring{ til::u8u16(Json::writeString(_getJsonStyledWriter(), schemeJson)) }));
}
if (applyToUserSettings)
{
// Don't add the color scheme to the Fragment's GlobalSettings; that will
// cause layering issues later. Add them to a staging area for later processing.
// (search for STAGED COLORS to find the next step)
settings.colorSchemes.emplace(scheme->Name(), std::move(scheme));
}
}
}
CATCH_LOG()
}
if (buildFragmentSettings)
{
fragmentSettings->ColorSchemes(fragmentColorSchemes.empty() ? nullptr : single_threaded_vector<Model::FragmentColorSchemeEntry>(std::move(fragmentColorSchemes)));
}
if (applyToUserSettings)
{
// Parse out actions from the fragment. Manually opt-out of keybinding
// parsing - fragments shouldn't be allowed to bind actions to keys
// directly. We may want to revisit circa GH#2205
settings.globals = winrt::make_self<GlobalAppSettings>();
settings.globals->SourceBasePath = sourceBasePath;
settings.globals->LayerActionsFrom(json.root, OriginTag::Fragment, false);
}
}
// Load new and modified profiles
{
if (applyToUserSettings)
{
const auto size = json.profilesList.size();
settings.profiles.reserve(size);
settings.profilesByGuid.reserve(size);
}
std::vector<Model::FragmentProfileEntry> newProfiles;
std::vector<Model::FragmentProfileEntry> modifiedProfiles;
for (const auto& profileJson : json.profilesList)
{
try
{
// GH#9962: Discard Guid-less, Name-less profiles, but...
// allow ones with an Updates field, as those are special for fragments.
// We need to make sure to only call Guid() if HasGuid() is true,
// as Guid() will dynamically generate a return value otherwise.
auto profile = _parseProfile(OriginTag::Fragment, source, profileJson);
const auto guid = profile->HasGuid() ? profile->Guid() : profile->Updates();
auto destinationSet = profile->HasGuid() ? &newProfiles : &modifiedProfiles;
if (guid != winrt::guid{})
{
profile->SourceBasePath = sourceBasePath;
if (buildFragmentSettings)
{
destinationSet->emplace_back(winrt::make<FragmentProfileEntry>(guid, hstring{ til::u8u16(Json::writeString(_getJsonStyledWriter(), profileJson)) }));
}
if (applyToUserSettings)
{
_appendProfile(std::move(profile), guid, settings);
}
}
}
CATCH_LOG()
}
if (buildFragmentSettings)
{
fragmentSettings->NewProfiles(newProfiles.empty() ? nullptr : single_threaded_vector<Model::FragmentProfileEntry>(std::move(newProfiles)));
fragmentSettings->ModifiedProfiles(modifiedProfiles.empty() ? nullptr : single_threaded_vector<Model::FragmentProfileEntry>(std::move(modifiedProfiles)));
_registerFragment(std::move(*fragmentSettings), fragmentMeta->scope);
}
}
// Merge profiles, color schemes, and globals into the user settings (aka inheritance)
if (applyToUserSettings)
{
for (const auto& fragmentProfile : settings.profiles)
{
if (const auto updates = fragmentProfile->Updates(); updates != winrt::guid{})
{
if (const auto it = userSettings.profilesByGuid.find(updates); it != userSettings.profilesByGuid.end())
{
it->second->AddMostImportantParent(fragmentProfile);
}
}
else
{
_addUserProfileParent(fragmentProfile);
}
}
// STAGED COLORS are processed here: we merge them into the partially-loaded
// settings directly so that we can resolve conflicts between user-generated
// color schemes and fragment-originated ones.
for (const auto& [_, fragmentColorScheme] : settings.colorSchemes)
{
_addOrMergeUserColorScheme(fragmentColorScheme);
}
// Add the parsed fragment globals as a parent of the user's settings.
// Later, in FinalizeInheritance, this will result in the action map from
// the fragments being applied before the user's own settings.
userSettings.globals->AddLeastImportantParent(settings.globals);
}
}
SettingsLoader::JsonSettings SettingsLoader::_parseJson(const std::string_view& content)
{
auto root = content.empty() ? Json::Value{ Json::ValueType::objectValue } : _parseJSON(content);
const auto& colorSchemes = _getJSONValue(root, SchemesKey);
const auto& themes = _getJSONValue(root, ThemesKey);
const auto& profilesObject = _getJSONValue(root, ProfilesKey);
const auto& profileDefaults = _getJSONValue(profilesObject, DefaultSettingsKey);
const auto& profilesList = profilesObject.isArray() ? profilesObject : _getJSONValue(profilesObject, ProfilesListKey);
return JsonSettings{ std::move(root), colorSchemes, profileDefaults, profilesList, themes };
}
// Just a common helper function between _parse and _parseFragment.
// Parses a profile and ensures it has a Guid if possible.
winrt::com_ptr<Profile> SettingsLoader::_parseProfile(const OriginTag origin, const winrt::hstring& source, const Json::Value& 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);
}
// If none exists. the Guid() getter generates one from Name() and optionally Source().
// 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->HasName())
{
profile->Guid(profile->Guid());
}
return profile;
}
// 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, const winrt::guid& guid, 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(guid, static_cast<const winrt::com_ptr<Profile>&>(profile)).second)
{
settings.profiles.emplace_back(profile);
}
else
{
duplicateProfile = true;
}
}
// If the given ParsedSettings instance contains a profile with the given profile's GUID,
// the profile is added as a parent. Otherwise, a new child profile is created.
void SettingsLoader::_addUserProfileParent(const winrt::com_ptr<implementation::Profile>& profile)
{
if (const auto [it, inserted] = userSettings.profilesByGuid.emplace(profile->Guid(), nullptr); !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->AddLeastImportantParent(profile);
}
else
{
// If inserted is true, then this is a generated profile that doesn't exist
// in the user's settings (which makes this branch somewhat unlikely).
//
// When a user modifies a profile they shouldn't modify the (static/constant)
// inbox profile of course. That's why we need to create a child.
// And since we previously added the (now) parent profile into profilesByGuid
// we'll have to replace it->second with the (new) child profile.
//
// These additional things are required to complete a (user) profile:
// * A call to _FinalizeInheritance()
// * Every profile should at least have Origin(), Name() and Hidden() set
// They're handled by SettingsLoader::FinalizeLayering() and detected by
// the missing Origin(). Setting these fields as late as possible ensures
// that we pick up the correct, inherited values of all of the child's parents.
//
// If you add more fields here, make sure to do the same in
// implementation::CreateChild().
auto child = winrt::make_self<Profile>();
child->AddLeastImportantParent(profile);
child->Guid(profile->Guid());
// If profile is a dynamic/generated profile, a fragment's
// Source() should have no effect on this user profile.
if (profile->HasSource())
{
child->Source(profile->Source());
}
it->second = child;
userSettings.profiles.emplace_back(std::move(child));
}
}
// returns whether the scheme was successfully added
bool SettingsLoader::_addOrMergeUserColorScheme(const winrt::com_ptr<implementation::ColorScheme>& newScheme)
{
// On entry, all the user color schemes have been loaded. Therefore, any insertions of inbox or fragment schemes
// will fail; we can leverage this to detect when they are equivalent and delete the user's duplicate copies.
// If the user has changed the otherwise "duplicate" scheme, though, we will move it aside.
if (const auto [it, inserted] = userSettings.colorSchemes.emplace(newScheme->Name(), newScheme); !inserted)
{
// This scheme was not inserted because one already existed.
auto existingScheme{ it->second };
if (existingScheme->Origin() == OriginTag::User) // we only want to impose ordering on User schemes
{
it->second = newScheme; // Stomp the user's existing scheme with the one we just got (to make sure the right Origin is set)
userSettings.fixupsAppliedDuringLoad = true; // Make sure we save the settings.
if (!existingScheme->IsEquivalentForSettingsMergePurposes(newScheme))
{
hstring newName{ fmt::format(FMT_COMPILE(L"{} (modified)"), existingScheme->Name()) };
int differentiator = 2;
while (userSettings.colorSchemes.contains(newName))
{
newName = hstring{ fmt::format(FMT_COMPILE(L"{} (modified {})"), existingScheme->Name(), differentiator++) };
}
// Rename the user's scheme.
existingScheme->Name(newName);
userSettings.colorSchemeRemappings.emplace(newScheme->Name(), newName);
// And re-add it to the end.
userSettings.colorSchemes.emplace(newName, std::move(existingScheme));
return true;
}
}
return false;
}
return true;
}
// As the name implies it executes a generator.
// Generated profiles are added to .inboxSettings. Used by GenerateProfiles().
void SettingsLoader::_executeGenerator(const IDynamicProfileGenerator& generator, std::vector<winrt::com_ptr<implementation::Profile>>& profilesList)
{
const auto generatorNamespace = generator.GetNamespace();
const auto previousSize = profilesList.size();
try
{
generator.GenerateProfiles(profilesList);
}
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 (profilesList.size() > previousSize)
{
const winrt::hstring source{ generatorNamespace };
for (const auto& profile : std::span(profilesList).subspan(previousSize))
{
profile->Origin(OriginTag::Generated);
profile->Source(source);
}
}
}
winrt::com_ptr<ExtensionPackage> SettingsLoader::_registerFragment(const winrt::Microsoft::Terminal::Settings::Model::FragmentSettings& fragment, FragmentScope scope)
{
winrt::com_ptr<ExtensionPackage> extPkg{ nullptr };
const auto src = fragment.Source();
const auto found = extensionPackageMap.find(src);
if (found != extensionPackageMap.end())
{
// retrieve from extensionPackageMap
extPkg = found->second;
}
else
{
// create a new entry in extensionPackageMap
const auto em = extensionPackageMap.emplace(src, winrt::make_self<ExtensionPackage>(src, scope));
extPkg = em.first->second;
}
extPkg->Fragments().Append(fragment);
return extPkg;
}
// 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
{
FILETIME lastWriteTime{};
auto settingsString = til::io::read_file_as_utf8_string_if_exists(_settingsPath(), false, &lastWriteTime);
auto firstTimeSetup = settingsString.empty();
// If it's the firstTimeSetup and a preview build, then try to
// read settings.json from the Release stable file path if it exists.
// Otherwise use default settings file provided from original settings file
bool releaseSettingExists = false;
if (firstTimeSetup && !IsPortableMode())
{
#if defined(WT_BRANDING_PREVIEW) || defined(WT_BRANDING_CANARY)
{
try
{
settingsString = til::io::read_file_as_utf8_string_if_exists(_releaseSettingsPath());
releaseSettingExists = settingsString.empty() ? false : true;
}
catch (...)
{
}
}
#endif
}
// GH#11119: If we find that the settings file doesn't exist, or is empty,
// then let's quick delete the state file as well. If the user does have a
// state file, and not a settings, then they probably tried to reset their
// settings. It might have data in it that was only relevant for a previous
// iteration of the settings file. If we don't, we'll load the old state and
// ignore all dynamic profiles (for example)!
if (firstTimeSetup)
{
ApplicationState::SharedInstance().Reset();
}
// Only uses default settings when firstTimeSetup is true and releaseSettingExists is false
// Otherwise use existing settingsString
const auto settingsStringView = (firstTimeSetup && !releaseSettingExists) ? LoadStringResource(IDR_USER_DEFAULTS) : settingsString;
auto mustWriteToDisk = firstTimeSetup;
SettingsLoader loader{ settingsStringView, LoadStringResource(IDR_DEFAULTS) };
winrt::hstring baseUserSettingsPath{ GetBaseSettingsPath().native() };
loader.userSettings.baseLayerProfile->SourceBasePath = baseUserSettingsPath;
loader.userSettings.globals->SourceBasePath = baseUserSettingsPath;
for (auto&& userProfile : loader.userSettings.profiles)
{
userProfile->SourceBasePath = baseUserSettingsPath;
}
// 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.
// Doesn't run when there is a Release settings.json that exists
if (firstTimeSetup && !releaseSettingExists)
{
loader.ApplyRuntimeInitialSettings();
}
loader.MergeInboxIntoUserSettings();
// Fragments might reference user profiles created by a generator.
// --> FindFragmentsAndMergeIntoUserSettings must be called after MergeInboxIntoUserSettings.
loader.FindFragmentsAndMergeIntoUserSettings(false /*generateExtensionPackages*/);
loader.FinalizeLayering();
// DisableDeletedProfiles returns true whenever we encountered any new generated/dynamic profiles.
// Similarly FixupUserSettings returns true, when it encountered settings that were patched up.
mustWriteToDisk |= loader.DisableDeletedProfiles();
mustWriteToDisk |= loader.AddDynamicProfileFolders();
mustWriteToDisk |= loader.FixupUserSettings();
// 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)
{
settings->WriteSettingsToDisk();
}
else
{
// lastWriteTime is only valid if mustWriteToDisk is false.
// Additionally WriteSettingsToDisk() updates the _hash for us already.
settings->_hash = _calculateHash(settingsString, lastWriteTime);
}
settings->_researchOnLoad();
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;
}
void CascadiaSettings::_researchOnLoad()
{
// Only do this if we're actually being sampled
if (TraceLoggingProviderEnabled(g_hSettingsModelProvider, 0, MICROSOFT_KEYWORD_MEASURES))
{
// ----------------------------- RE: Themes ----------------------------
const auto numThemes = GlobalSettings().Themes().Size();
const auto themeInUse = GlobalSettings().CurrentTheme().Name();
const auto changedTheme = GlobalSettings().HasTheme();
// system: 0
// light: 1
// dark: 2
// a custom theme: 3
// system (legacy): 4
// light (legacy): 5
// dark (legacy): 6
const auto themeChoice = themeInUse == L"system" ? 0 :
themeInUse == L"light" ? 1 :
themeInUse == L"dark" ? 2 :
themeInUse == L"legacyDark" ? 4 :
themeInUse == L"legacyLight" ? 5 :
themeInUse == L"legacySystem" ? 6 :
3;
TraceLoggingWrite(
g_hSettingsModelProvider,
"ThemesInUse",
TraceLoggingDescription("Data about the themes in use"),
TraceLoggingBool(themeChoice, "Identifier for the theme chosen. 0 is system, 1 is light, 2 is dark, and 3 indicates any custom theme."),
TraceLoggingBool(changedTheme, "True if the user actually changed the theme from the default theme"),
TraceLoggingInt32(numThemes, "Number of themes in the user's settings"),
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage));
// --------------------------- RE: sendInput ---------------------------
auto collectSendInput = [&]() {
auto totalSendInput = 0;
const auto& allActions = GlobalSettings().ActionMap().AvailableActions();
for (const auto&& [name, actionAndArgs] : allActions)
{
if (actionAndArgs.Action() == ShortcutAction::SendInput)
{
totalSendInput++;
}
}
return totalSendInput;
};
TraceLoggingWrite(
g_hSettingsModelProvider,
"SendInputUsage",
TraceLoggingDescription("Event emitted upon settings load, containing the number of sendInput actions a user has"),
TraceLoggingInt32(collectSendInput(), "Number of sendInput actions in the user's settings"),
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage));
// ------------------------ RE: autoMarkPrompts ------------------------
auto totalAutoMark = 0;
auto totalShowMarks = 0;
for (const auto&& p : AllProfiles())
{
totalAutoMark += p.AutoMarkPrompts() ? 1 : 0;
totalShowMarks += p.ShowMarks() ? 1 : 0;
}
TraceLoggingWrite(
g_hSettingsModelProvider,
"MarksProfilesUsage",
TraceLoggingDescription("Event emitted upon settings load, containing the number of profiles opted-in to scrollbar marks"),
TraceLoggingInt32(totalAutoMark, "Number of profiles for which AutoMarkPrompts is enabled"),
TraceLoggingInt32(totalShowMarks, "Number of profiles for which ShowMarks is enabled"),
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage));
}
}
// Function Description:
// - Creates a new CascadiaSettings object initialized with settings from the
// hard-coded 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{}, LoadStringResource(IDR_DEFAULTS));
}
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) :
// The CascadiaSettings class declaration initializes these fields by default,
// but we're going to set these fields in our constructor later on anyways.
_globals{},
_baseLayerProfile{},
_allProfiles{},
_activeProfiles{},
_warnings{}
{
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& colorScheme : loader.userSettings.colorSchemes)
{
loader.userSettings.globals->AddColorScheme(*colorScheme.second);
}
// 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);
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; }))
{
profile->Orphaned(true);
}
}
allProfiles.emplace_back(*profile);
if (!profile->Hidden() && !profile->Orphaned())
{
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);
}
_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));
_themesChangeLog = std::move(loader.userSettings.themesChangeLog);
_resolveDefaultProfile();
_resolveNewTabMenuProfiles();
_validateSettings();
ExpandCommands();
}
// 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;
}
// Method Description:
// - Returns the path of the settings.json file from stable file path
// Arguments:
// - <none>
// Return Value:
// - Path to stable settings
const std::filesystem::path& CascadiaSettings::_releaseSettingsPath()
{
static const auto path = GetReleaseSettingsPath() / SettingsFilename;
return path;
}
// Returns a has (approximately) uniquely identifying the settings.json contents on disk.
winrt::hstring CascadiaSettings::_calculateHash(std::string_view settings, const FILETIME& lastWriteTime)
{
const auto fileHash = til::hash(settings);
const ULARGE_INTEGER fileTime{ lastWriteTime.dwLowDateTime, lastWriteTime.dwHighDateTime };
const auto hash = fmt::format(FMT_COMPILE(L"{:016x}-{:016x}"), fileHash, fileTime.QuadPart);
return winrt::hstring{ hash };
}
// This returns something akin to %LOCALAPPDATA%\Packages\WindowsTerminalDev_8wekyb3d8bbwe\LocalState
// just like SettingsPath(), but without the trailing \settings.json.
winrt::hstring CascadiaSettings::SettingsDirectory()
{
return winrt::hstring{ GetBaseSettingsPath().native() };
}
// 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() };
}
bool CascadiaSettings::IsPortableMode()
{
return Model::IsPortableMode();
}
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() };
}
void CascadiaSettings::ResetApplicationState() const
{
auto state = ApplicationState::SharedInstance();
const auto hash = state.SettingsHash();
state.Reset();
state.SettingsHash(hash);
state.Flush();
}
void CascadiaSettings::ResetToDefaultSettings()
{
ApplicationState::SharedInstance().Reset();
_writeSettingsToDisk(LoadStringResource(IDR_USER_DEFAULTS));
}
// 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>
bool CascadiaSettings::WriteSettingsToDisk()
{
// write current settings to current settings file
Json::StreamWriterBuilder wbuilder;
wbuilder.settings_["enableYAMLCompatibility"] = true; // suppress spaces around colons
wbuilder.settings_["indentation"] = " ";
wbuilder.settings_["precision"] = 6; // prevent values like 1.1000000000000001
try
{
_writeSettingsToDisk(Json::writeString(wbuilder, ToJson()));
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
_warnings.Append(SettingsLoadWarnings::FailedToWriteToSettings);
return false;
}
return true;
}
void CascadiaSettings::_writeSettingsToDisk(std::string_view contents)
{
FILETIME lastWriteTime{};
til::io::write_utf8_string_to_file_atomic(_settingsPath(), contents, &lastWriteTime);
_hash = _calculateHash(contents, lastWriteTime);
// Persists the default terminal choice
// GH#10003 - Only do this if _currentDefaultTerminal was actually initialized.
if (_currentDefaultTerminal)
{
DefaultTerminal::Current(_currentDefaultTerminal);
}
}
#ifndef NDEBUG
[[maybe_unused]] static std::string _getDevPathToSchema()
{
std::filesystem::path filePath{ __FILE__ };
auto schemaPath = filePath.parent_path().parent_path().parent_path().parent_path() / "doc" / "cascadia" / "profiles.schema.json";
return "file:///" + schemaPath.generic_string();
}
#endif
// 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
auto json{ _globals->ToJson() };
json["$help"] = "https://aka.ms/terminal-documentation";
json["$schema"] =
#if defined(WT_BRANDING_RELEASE)
"https://aka.ms/terminal-profiles-schema"
#elif defined(WT_BRANDING_PREVIEW)
"https://aka.ms/terminal-profiles-schema-preview"
#elif !defined(NDEBUG) // DEBUG mode
_getDevPathToSchema() // magic schema path that refers to the local source directory
#else // All other brandings
"https://raw.githubusercontent.com/microsoft/terminal/main/doc/cascadia/profiles.schema.json"
#endif
;
// "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;
Json::Value schemes{ Json::ValueType::arrayValue };
for (const auto& entry : _globals->ColorSchemes())
{
const auto scheme{ winrt::get_self<ColorScheme>(entry.Value()) };
if (scheme->Origin() == OriginTag::User)
{
schemes.append(scheme->ToJson());
}
}
json[JsonKey(SchemesKey)] = schemes;
Json::Value themes{ Json::ValueType::arrayValue };
for (const auto& entry : _globals->Themes())
{
// Ignore the built in themes, when serializing the themes back out. We
// don't want to re-include them in the user settings file.
const auto theme{ winrt::get_self<Theme>(entry.Value()) };
const auto& name{ theme->Name() };
if (std::ranges::find(builtinThemes, name) != builtinThemes.end())
{
continue;
}
themes.append(theme->ToJson());
}
json[JsonKey(ThemesKey)] = themes;
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());
}
// Method Description:
// - Iterates through the "newTabMenu" entries and for ProfileEntries resolves the "profile"
// fields, which can be a profile name, to a GUID and stores it back.
// - It finds any "source" entries and finds all profiles generated by that source
// - Lastly, it finds any "remainingProfiles" entries and stores which profiles they
// represent (those that were not resolved before). It adds a warning when
// multiple of these entries are found.
void CascadiaSettings::_resolveNewTabMenuProfiles() const
{
Model::RemainingProfilesEntry remainingProfilesEntry = nullptr;
// The TerminalPage needs to know which profile has which profile ID. To prevent
// continuous lookups in the _activeProfiles vector, we create a map <int, Profile>
// to store these indices in-flight.
auto remainingProfilesMap = std::map<int, Model::Profile>{};
auto activeProfileCount = gsl::narrow_cast<int>(_activeProfiles.Size());
for (auto profileIndex = 0; profileIndex < activeProfileCount; profileIndex++)
{
remainingProfilesMap.emplace(profileIndex, _activeProfiles.GetAt(profileIndex));
}
// We keep track of the "remaining profiles" - those that have not yet been resolved
// in either a "profile" or "source" entry. They will possibly be assigned to a
// "remainingProfiles" entry
auto remainingProfiles = single_threaded_map(std::move(remainingProfilesMap));
// We call a recursive helper function to process the entries
auto entries = _globals->NewTabMenu();
_resolveNewTabMenuProfilesSet(entries, remainingProfiles, remainingProfilesEntry);
// If a "remainingProfiles" entry has been found, assign to it the remaining profiles
if (remainingProfilesEntry != nullptr)
{
remainingProfilesEntry.Profiles(remainingProfiles);
}
// If the configuration does not have a "newTabMenu" field, GlobalAppSettings
// will return a default value containing just a "remainingProfiles" entry. However,
// this value is regenerated on every "get" operation, so the effect of setting
// the remaining profiles above will be undone. So only in the case that no custom
// value is present in GlobalAppSettings, we will store the modified default value.
if (!_globals->HasNewTabMenu())
{
_globals->NewTabMenu(entries);
}
}
// Method Description:
// - Helper function that processes a set of tab menu entries and resolves any profile names
// or source fields as necessary - see function above for a more detailed explanation.
void CascadiaSettings::_resolveNewTabMenuProfilesSet(const IVector<Model::NewTabMenuEntry> entries, IMap<int, Model::Profile>& remainingProfilesMap, Model::RemainingProfilesEntry& remainingProfilesEntry) const
{
if (entries == nullptr || entries.Size() == 0)
{
return;
}
for (const auto& entry : entries)
{
if (entry == nullptr)
{
continue;
}
switch (entry.Type())
{
// For a simple profile entry, the "profile" field can either be a name or a GUID. We
// use the GetProfileByName function to resolve this name to a profile instance, then
// find the index of that profile, and store this information in the entry.
case NewTabMenuEntryType::Profile:
{
// We need to access the unresolved profile name, a field that is not exposed
// in the projected class. So, we need to first obtain our implementation struct
// instance, to access this field.
const auto profileEntry{ winrt::get_self<implementation::ProfileEntry>(entry.as<Model::ProfileEntry>()) };
// Find the profile by name
const auto profile = GetProfileByName(profileEntry->ProfileName());
// If not found, or if the profile is hidden, skip it
if (profile == nullptr || profile.Hidden())
{
profileEntry->Profile(nullptr); // override "default" profile
break;
}
// Find the index of the resulting profile and store the result in the entry
uint32_t profileIndex;
_activeProfiles.IndexOf(profile, profileIndex);
profileEntry->Profile(profile);
profileEntry->ProfileIndex(profileIndex);
// Remove from remaining profiles list (map)
remainingProfilesMap.TryRemove(profileIndex);
break;
}
// For a remainingProfiles entry, we store it in the variable that is passed back to our caller,
// except when that one has already been set (so we found a second/third/...) instance, which will
// trigger a warning. We then ignore this entry.
case NewTabMenuEntryType::RemainingProfiles:
{
if (remainingProfilesEntry != nullptr)
{
_warnings.Append(SettingsLoadWarnings::DuplicateRemainingProfilesEntry);
}
else
{
remainingProfilesEntry = entry.as<Model::RemainingProfilesEntry>();
}
break;
}
// For a folder, we simply call this method recursively
case NewTabMenuEntryType::Folder:
{
// We need to access the unfiltered entry list, a field that is not exposed
// in the projected class. So, we need to first obtain our implementation struct
// instance, to access this field.
const auto folderEntry{ winrt::get_self<implementation::FolderEntry>(entry.as<Model::FolderEntry>()) };
auto folderEntries = folderEntry->RawEntries();
_resolveNewTabMenuProfilesSet(folderEntries, remainingProfilesMap, remainingProfilesEntry);
break;
}
// For a "matchProfiles" entry, we iterate through the list of all profiles and
// find all those matching: generated by the same source, having the same name, or
// having the same commandline. This can be expanded with regex support in the future.
// We make sure that none of the matches are included in the "remaining profiles" section.
case NewTabMenuEntryType::MatchProfiles:
{
// We need to access the matching function, which is not exposed in the projected class.
// So, we need to first obtain our implementation struct instance, to access this field.
const auto matchEntry{ winrt::get_self<implementation::MatchProfilesEntry>(entry.as<Model::MatchProfilesEntry>()) };
matchEntry->Profiles(single_threaded_map<int, Model::Profile>());
auto activeProfileCount = gsl::narrow_cast<int>(_activeProfiles.Size());
for (auto profileIndex = 0; profileIndex < activeProfileCount; profileIndex++)
{
const auto profile = _activeProfiles.GetAt(profileIndex);
// On a match, we store it in the entry and remove it from the remaining list
if (matchEntry->MatchesProfile(profile))
{
matchEntry->Profiles().Insert(profileIndex, profile);
remainingProfilesMap.TryRemove(profileIndex);
}
}
break;
}
}
}
}
void CascadiaSettings::LogSettingChanges(bool isJsonLoad) const
{
#ifndef _DEBUG
// Only do this if we're actually being sampled
if (!TraceLoggingProviderEnabled(g_hSettingsModelProvider, 0, MICROSOFT_KEYWORD_MEASURES))
{
return;
}
#endif // !_DEBUG
// aggregate setting changes
std::set<std::string> changes;
static constexpr std::string_view globalContext{ "global" };
_globals->LogSettingChanges(changes, globalContext);
// Actions are not expected to change when loaded from the settings UI
static constexpr std::string_view actionContext{ "action" };
winrt::get_self<implementation::ActionMap>(_globals->ActionMap())->LogSettingChanges(changes, actionContext);
static constexpr std::string_view profileContext{ "profile" };
for (const auto& profile : _allProfiles)
{
winrt::get_self<Profile>(profile)->LogSettingChanges(changes, profileContext);
}
static constexpr std::string_view profileDefaultsContext{ "profileDefaults" };
_baseLayerProfile->LogSettingChanges(changes, profileDefaultsContext);
// Themes are not expected to change when loaded from the settings UI
// DO NOT CALL Theme::LogSettingChanges!!
// We already collected the changes when we loaded the JSON
for (const auto& change : _themesChangeLog)
{
changes.insert(change);
}
#if defined(WT_BRANDING_RELEASE)
constexpr uint8_t branding = 3;
#elif defined(WT_BRANDING_PREVIEW)
constexpr uint8_t branding = 2;
#elif defined(WT_BRANDING_CANARY)
constexpr uint8_t branding = 1;
#else
constexpr uint8_t branding = 0;
#endif
const uint8_t distribution = IsPackaged() ? 2 :
IsPortableMode() ? 1 :
0;
// report changes
for (const auto& change : changes)
{
// A `isJsonLoad ? "JsonSettingsChanged" : "UISettingsChanged"`
// would be nice, but that apparently isn't allowed in the macro below.
// Also, there's guidance to not send too much data all in one event,
// so we'll be sending a ton of events here.
if (isJsonLoad)
{
TraceLoggingWrite(g_hSettingsModelProvider,
"JsonSettingsChanged",
TraceLoggingDescription("Event emitted when settings.json change"),
TraceLoggingValue(change.data(), "Setting"),
TraceLoggingValue(branding, "Branding"),
TraceLoggingValue(distribution, "Distribution"),
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage));
}
else
{
TraceLoggingWrite(g_hSettingsModelProvider,
"UISettingsChanged",
TraceLoggingDescription("Event emitted when settings change via the UI"),
TraceLoggingValue(change.data(), "Setting"),
TraceLoggingValue(branding, "Branding"),
TraceLoggingValue(distribution, "Distribution"),
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage));
}
}
}