// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #include "pch.h" #include "CascadiaSettings.h" #include #include #include #include #include #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 static auto extractValueFromTaskWithoutMainThreadAwait(TTask&& task) -> decltype(task.get()) { std::optional 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; } // 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(len)); p += len + 1; } break; } } // 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(VisualStudioGenerator{}); #if TIL_FEATURE_DYNAMICSSHPROFILES_ENABLED _executeGenerator(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() { 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 = til::io::read_file_as_utf8_string_if_exists(fragmentExt.path()); if (!content.empty()) { _parseFragment(source, content, fragmentSettings); } } 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.contains(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". // // 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 extensions; try { const auto catalog = AppExtensionCatalog::Open(AppExtensionHostName); extensions = extractValueFromTaskWithoutMainThreadAwait(catalog.FindAllAsync()); } CATCH_LOG(); if (!extensions) { return; } for (const auto& ext : extensions) { const auto packageName = ext.Package().Id().FamilyName(); if (_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)) { parseAndLayerFragmentFiles(path, packageName); } } } // 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 std::string_view& content) { ParsedSettings fragmentSettings; _parseFragment(source, content, fragmentSettings); } // 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::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; } bool winrt::Microsoft::Terminal::Settings::Model::implementation::SettingsLoader::RemapColorSchemeForProfile(const winrt::com_ptr& 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() == 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 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(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 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> 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. void SettingsLoader::_parseFragment(const winrt::hstring& source, const std::string_view& content, ParsedSettings& settings) { auto json = _parseJson(content); settings.clear(); { settings.globals = winrt::make_self(); for (const auto& schemeJson : json.colorSchemes) { try { if (const auto scheme = ColorScheme::FromJson(schemeJson)) { scheme->Origin(OriginTag::Fragment); // 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() } // 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->LayerActionsFrom(json.root, OriginTag::Fragment, false); } { const auto size = json.profilesList.size(); settings.profiles.reserve(size); settings.profilesByGuid.reserve(size); for (const auto& profileJson : json.profilesList) { try { auto profile = _parseProfile(OriginTag::Fragment, source, profileJson); // 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. const auto guid = profile->HasGuid() ? profile->Guid() : profile->Updates(); if (guid != winrt::guid{}) { _appendProfile(std::move(profile), guid, settings); } } CATCH_LOG() } } 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.second); } // 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 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, 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&>(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& 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(); 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)); } } void SettingsLoader::_addOrMergeUserColorScheme(const winrt::com_ptr& 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)); } } } } // 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.contains(generatorNamespace)) { return; } const auto previousSize = inboxSettings.profiles.size(); try { generator.GenerateProfiles(inboxSettings.profiles); } CATCH_LOG_MSG("Dynamic Profile Namespace: \"%.*s\"", gsl::narrow(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 : std::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 { 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) }; // 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(); 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.FixupUserSettings(); // If this throws, the app will catch it and use the default settings. const auto settings = winrt::make_self(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); } } 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() }; settings->_loadError = ex.Error(); return *settings; } catch (const SettingsTypedDeserializationException& e) { const auto settings{ winrt::make_self() }; 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: // - // Return Value: // - a unique_ptr to a CascadiaSettings with the settings from defaults.json Model::CascadiaSettings CascadiaSettings::LoadDefaults() { return *winrt::make_self(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 allProfiles; std::vector activeProfiles; std::vector 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: // - // 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: // - // 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: // - // 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(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: // - // Return Value: // - void 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 _writeSettingsToDisk(Json::writeString(wbuilder, ToJson())); } 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: // - // 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(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(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(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 // to store these indices in-flight. auto remainingProfilesMap = std::map{}; auto activeProfileCount = gsl::narrow_cast(_activeProfiles.Size()); for (auto profileIndex = 0; profileIndex < activeProfileCount; profileIndex++) { remainingProfilesMap.emplace(profileIndex, _activeProfiles.GetAt(profileIndex)); } // We keep track of the "remaining profiles" - those that have not yet been resolved // in either a "profile" or "source" entry. They will possibly be assigned to a // "remainingProfiles" entry auto remainingProfiles = single_threaded_map(std::move(remainingProfilesMap)); // We call a recursive helper function to process the entries auto entries = _globals->NewTabMenu(); _resolveNewTabMenuProfilesSet(entries, remainingProfiles, remainingProfilesEntry); // If a "remainingProfiles" entry has been found, assign to it the remaining profiles if (remainingProfilesEntry != nullptr) { remainingProfilesEntry.Profiles(remainingProfiles); } // If the configuration does not have a "newTabMenu" field, GlobalAppSettings // will return a default value containing just a "remainingProfiles" entry. However, // this value is regenerated on every "get" operation, so the effect of setting // the remaining profiles above will be undone. So only in the case that no custom // value is present in GlobalAppSettings, we will store the modified default value. if (!_globals->HasNewTabMenu()) { _globals->NewTabMenu(entries); } } // Method Description: // - Helper function that processes a set of tab menu entries and resolves any profile names // or source fields as necessary - see function above for a more detailed explanation. void CascadiaSettings::_resolveNewTabMenuProfilesSet(const IVector entries, IMap& remainingProfilesMap, Model::RemainingProfilesEntry& remainingProfilesEntry) const { if (entries == nullptr || entries.Size() == 0) { return; } for (const auto& entry : entries) { if (entry == nullptr) { continue; } switch (entry.Type()) { // For a simple profile entry, the "profile" field can either be a name or a GUID. We // use the GetProfileByName function to resolve this name to a profile instance, then // find the index of that profile, and store this information in the entry. case NewTabMenuEntryType::Profile: { // We need to access the unresolved profile name, a field that is not exposed // in the projected class. So, we need to first obtain our implementation struct // instance, to access this field. const auto profileEntry{ winrt::get_self(entry.as()) }; // Find the profile by name const auto profile = GetProfileByName(profileEntry->ProfileName()); // If not found, or if the profile is hidden, skip it if (profile == nullptr || profile.Hidden()) { profileEntry->Profile(nullptr); // override "default" profile break; } // Find the index of the resulting profile and store the result in the entry uint32_t profileIndex; _activeProfiles.IndexOf(profile, profileIndex); profileEntry->Profile(profile); profileEntry->ProfileIndex(profileIndex); // Remove from remaining profiles list (map) remainingProfilesMap.TryRemove(profileIndex); break; } // For a remainingProfiles entry, we store it in the variable that is passed back to our caller, // except when that one has already been set (so we found a second/third/...) instance, which will // trigger a warning. We then ignore this entry. case NewTabMenuEntryType::RemainingProfiles: { if (remainingProfilesEntry != nullptr) { _warnings.Append(SettingsLoadWarnings::DuplicateRemainingProfilesEntry); } else { remainingProfilesEntry = entry.as(); } break; } // For a folder, we simply call this method recursively case NewTabMenuEntryType::Folder: { // We need to access the unfiltered entry list, a field that is not exposed // in the projected class. So, we need to first obtain our implementation struct // instance, to access this field. const auto folderEntry{ winrt::get_self(entry.as()) }; auto folderEntries = folderEntry->RawEntries(); _resolveNewTabMenuProfilesSet(folderEntries, remainingProfilesMap, remainingProfilesEntry); break; } // For a "matchProfiles" entry, we iterate through the list of all profiles and // find all those matching: generated by the same source, having the same name, or // having the same commandline. This can be expanded with regex support in the future. // We make sure that none of the matches are included in the "remaining profiles" section. case NewTabMenuEntryType::MatchProfiles: { // We need to access the matching function, which is not exposed in the projected class. // So, we need to first obtain our implementation struct instance, to access this field. const auto matchEntry{ winrt::get_self(entry.as()) }; matchEntry->Profiles(single_threaded_map()); auto activeProfileCount = gsl::narrow_cast(_activeProfiles.Size()); for (auto profileIndex = 0; profileIndex < activeProfileCount; profileIndex++) { const auto profile = _activeProfiles.GetAt(profileIndex); // On a match, we store it in the entry and remove it from the remaining list if (matchEntry->MatchesProfile(profile)) { matchEntry->Profiles().Insert(profileIndex, profile); remainingProfilesMap.TryRemove(profileIndex); } } break; } } } } 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 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(_globals->ActionMap())->LogSettingChanges(changes, actionContext); static constexpr std::string_view profileContext{ "profile" }; for (const auto& profile : _allProfiles) { winrt::get_self(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)); } } }