diff --git a/.github/actions/spelling/dictionary/apis.txt b/.github/actions/spelling/dictionary/apis.txt index d3b3ac73bce..2a0bb0a6c4a 100644 --- a/.github/actions/spelling/dictionary/apis.txt +++ b/.github/actions/spelling/dictionary/apis.txt @@ -93,6 +93,8 @@ TBPF THEMECHANGED tmp tolower +TTask +TVal tx UPDATEINIFILE userenv diff --git a/src/cascadia/CascadiaPackage/Package-Dev.appxmanifest b/src/cascadia/CascadiaPackage/Package-Dev.appxmanifest index f4fa79fb4a5..f2dc8a93215 100644 --- a/src/cascadia/CascadiaPackage/Package-Dev.appxmanifest +++ b/src/cascadia/CascadiaPackage/Package-Dev.appxmanifest @@ -12,7 +12,7 @@ xmlns:desktop5="http://schemas.microsoft.com/appx/manifest/desktop/windows10/5" xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities" xmlns:uap5="http://schemas.microsoft.com/appx/manifest/uap/windows10/5" - IgnorableNamespaces="uap mp rescap"> + IgnorableNamespaces="uap mp rescap uap3"> + + + com.microsoft.windows.terminal.settings + + diff --git a/src/cascadia/CascadiaPackage/Package-Pre.appxmanifest b/src/cascadia/CascadiaPackage/Package-Pre.appxmanifest index 62fbb9de1db..296b733914e 100644 --- a/src/cascadia/CascadiaPackage/Package-Pre.appxmanifest +++ b/src/cascadia/CascadiaPackage/Package-Pre.appxmanifest @@ -13,7 +13,7 @@ xmlns:desktop4="http://schemas.microsoft.com/appx/manifest/desktop/windows10/4" xmlns:desktop5="http://schemas.microsoft.com/appx/manifest/desktop/windows10/5" xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities" - IgnorableNamespaces="uap mp rescap"> + IgnorableNamespaces="uap mp rescap uap3"> + + + com.microsoft.windows.terminal.settings + + + IgnorableNamespaces="uap mp rescap uap3"> + + + com.microsoft.windows.terminal.settings + + - + @@ -22,6 +22,13 @@ + + + + com.microsoft.windows.terminal.settings + + + diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettings.h b/src/cascadia/TerminalSettingsModel/CascadiaSettings.h index 953c9bf8f16..2dbeb4eda24 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettings.h +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettings.h @@ -117,6 +117,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation winrt::com_ptr _FindMatchingProfile(const Json::Value& profileJson); std::optional _FindMatchingProfileIndex(const Json::Value& profileJson); void _LayerOrCreateColorScheme(const Json::Value& schemeJson); + Json::Value _ParseUtf8JsonString(std::string_view fileData); + winrt::com_ptr _FindMatchingColorScheme(const Json::Value& schemeJson); void _ParseJsonString(std::string_view fileData, const bool isDefaultSettings); static const Json::Value& _GetProfilesJsonObject(const Json::Value& json); @@ -129,6 +131,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation void _ApplyDefaultsFromUserSettings(); void _LoadDynamicProfiles(); + void _LoadFragmentExtensions(); + void _ApplyJsonStubsHelper(const std::wstring_view directory, const std::unordered_set& ignoredNamespaces); + std::unordered_set _AccumulateJsonFilesInDirectory(const std::wstring_view directory); + void _ParseAndLayerFragmentFiles(const std::unordered_set files, const winrt::hstring source); static bool _IsPackaged(); static void _WriteSettings(std::string_view content, const hstring filepath); diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp index 3cbb4419066..a39f609373c 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp @@ -10,6 +10,7 @@ #include #include #include +#include "DefaultProfileUtils.h" // defaults.h is a file containing the default json settings in a std::string_view #include "defaults.h" @@ -36,6 +37,9 @@ static constexpr std::string_view ProfilesListKey{ "list" }; static constexpr std::string_view LegacyKeybindingsKey{ "keybindings" }; static constexpr std::string_view ActionsKey{ "actions" }; static constexpr std::string_view SchemesKey{ "schemes" }; +static constexpr std::string_view NameKey{ "name" }; +static constexpr std::string_view UpdatesKey{ "updates" }; +static constexpr std::string_view GuidKey{ "guid" }; static constexpr std::string_view DisabledProfileSourcesKey{ "disabledProfileSources" }; @@ -43,6 +47,39 @@ static constexpr std::string_view Utf8Bom{ u8"\uFEFF" }; static constexpr std::string_view SettingsSchemaFragment{ "\n" R"( "$schema": "https://aka.ms/terminal-profiles-schema")" }; +static constexpr std::string_view jsonExtension{ ".json" }; +static constexpr std::string_view FragmentsSubDirectory{ "\\Fragments" }; +static constexpr std::wstring_view FragmentsPath{ L"\\Microsoft\\Windows Terminal\\Fragments" }; + +static constexpr std::string_view AppExtensionHostName{ "com.microsoft.windows.terminal.settings" }; + +// 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()) +{ + using TVal = decltype(task.get()); + std::optional finalVal{}; + std::condition_variable cv; + std::mutex mtx; + + auto waitOnBackground = [&]() -> winrt::fire_and_forget { + co_await winrt::resume_background(); + auto v{ co_await task }; + + std::unique_lock lock{ mtx }; + finalVal.emplace(std::move(v)); + cv.notify_all(); + }; + + std::unique_lock lock{ mtx }; + waitOnBackground(); + cv.wait(lock, [&]() { return finalVal.has_value(); }); + return *finalVal; +} + static std::tuple _LineAndColumnFromPosition(const std::string_view string, ptrdiff_t position) { size_t line = 1, column = position + 1; @@ -136,6 +173,11 @@ winrt::Microsoft::Terminal::Settings::Model::CascadiaSettings CascadiaSettings:: // created by now, because we're going to check in there for any generators // that should be disabled (if the user had any settings.) resultPtr->_LoadDynamicProfiles(); + try + { + resultPtr->_LoadFragmentExtensions(); + } + CATCH_LOG(); if (!fileHasData) { @@ -383,6 +425,216 @@ void CascadiaSettings::_LoadDynamicProfiles() } } +// Method Description: +// - Searches the local app data folder, global app data folder and app +// extensions for json stubs we should use to create new profiles, +// modify existing profiles or add new color schemes +// - If the user settings has any namespaces in the "disabledProfileSources" +// property, we'll ensure that the corresponding folders do not get searched +void CascadiaSettings::_LoadFragmentExtensions() +{ + // First, accumulate the namespaces the user wants to ignore + std::unordered_set ignoredNamespaces; + const auto disabledProfileSources = CascadiaSettings::_GetDisabledProfileSourcesJsonObject(_userSettings); + if (disabledProfileSources.isArray()) + { + for (const auto& json : disabledProfileSources) + { + ignoredNamespaces.emplace(JsonUtils::GetValue(json)); + } + } + + // Search through the local app data folder + wil::unique_cotaskmem_string localAppDataFolder; + THROW_IF_FAILED(SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &localAppDataFolder)); + auto localAppDataFragments = std::wstring(localAppDataFolder.get()) + FragmentsPath.data(); + + if (std::filesystem::exists(localAppDataFragments)) + { + _ApplyJsonStubsHelper(localAppDataFragments, ignoredNamespaces); + } + + // Search through the program data folder + wil::unique_cotaskmem_string programDataFolder; + THROW_IF_FAILED(SHGetKnownFolderPath(FOLDERID_ProgramData, 0, nullptr, &programDataFolder)); + auto programDataFragments = std::wstring(programDataFolder.get()) + FragmentsPath.data(); + if (std::filesystem::exists(programDataFragments)) + { + _ApplyJsonStubsHelper(programDataFragments, ignoredNamespaces); + } + + // Search through app extensions + // Gets the catalog of extensions with the name "com.microsoft.windows.terminal.settings" + const auto catalog = Windows::ApplicationModel::AppExtensions::AppExtensionCatalog::Open(winrt::to_hstring(AppExtensionHostName)); + + auto extensions = _extractValueFromTaskWithoutMainThreadAwait(catalog.FindAllAsync()); + + for (const auto& ext : extensions) + { + // Only apply the stubs if the package name is not in ignored namespaces + if (ignoredNamespaces.find(ext.Package().Id().FamilyName().c_str()) == ignoredNamespaces.end()) + { + // Likewise, getting the public folder from an extension is an async operation + // So we use another mutex and condition variable + auto foundFolder = _extractValueFromTaskWithoutMainThreadAwait(ext.GetPublicFolderAsync()); + + // 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 + auto path = winrt::to_string(foundFolder.Path()); + path.append(FragmentsSubDirectory); + + // If the directory exists, use the fragments in it + if (std::filesystem::exists(path)) + { + const auto jsonFiles = _AccumulateJsonFilesInDirectory(til::u8u16(path)); + + // Provide the package name as the source + _ParseAndLayerFragmentFiles(jsonFiles, ext.Package().Id().FamilyName().c_str()); + } + } + } +} + +// Method Description: +// - Helper function to apply json stubs in the local app data folder and the global program data folder +// Arguments: +// - The directory to find json files in +// - The set of ignored namespaces +void CascadiaSettings::_ApplyJsonStubsHelper(const std::wstring_view directory, const std::unordered_set& ignoredNamespaces) +{ + // The json files should be within subdirectories where the subdirectory name is the app name + for (const auto& fragmentExtFolder : std::filesystem::directory_iterator(directory)) + { + // We only want the parent folder name as the source (not the full path) + const auto source = fragmentExtFolder.path().filename().wstring(); + + // Only apply the stubs if the parent folder name is not in ignored namespaces + // (also make sure this is a directory for sanity) + if (std::filesystem::is_directory(fragmentExtFolder) && ignoredNamespaces.find(source) == ignoredNamespaces.end()) + { + const auto jsonFiles = _AccumulateJsonFilesInDirectory(fragmentExtFolder.path().c_str()); + _ParseAndLayerFragmentFiles(jsonFiles, winrt::hstring{ source }); + } + } +} + +// Method Description: +// - Finds all the json files within the given directory +// Arguments: +// - directory: the directory to search +// Return Value: +// - A set containing all the found file data +std::unordered_set CascadiaSettings::_AccumulateJsonFilesInDirectory(const std::wstring_view directory) +{ + std::unordered_set jsonFiles; + + for (const auto& fragmentExt : std::filesystem::directory_iterator(directory)) + { + if (fragmentExt.path().extension() == jsonExtension) + { + wil::unique_hfile hFile{ CreateFileW(fragmentExt.path().c_str(), + GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + nullptr) }; + + if (!hFile) + { + LOG_LAST_ERROR(); + } + else + { + const auto fileData = _ReadFile(hFile.get()).value(); + jsonFiles.emplace(fileData); + } + } + } + return jsonFiles; +} + +// Method Description: +// - Given a set of json files, uses them to modify existing profiles, +// create new profiles, and create new color schemes +// Arguments: +// - files: the set of json files (each item in the set is the file data) +// - source: the location the files came from +void CascadiaSettings::_ParseAndLayerFragmentFiles(const std::unordered_set files, const winrt::hstring source) +{ + for (const auto& file : files) + { + // A file could have many new profiles/many profiles it wants to modify/many new color schemes + // so we first parse the entire file into one json object + auto fullFile = _ParseUtf8JsonString(file.data()); + + if (fullFile.isMember(JsonKey(ProfilesKey))) + { + // Now we separately get each stub that modifies/adds a profile + // We intentionally don't use a const reference here because we modify + // the profile stub by giving it a guid so we can call _FindMatchingProfile + for (auto& profileStub : fullFile[JsonKey(ProfilesKey)]) + { + if (profileStub.isMember(JsonKey(UpdatesKey))) + { + // This stub is meant to be a modification to an existing profile, + // try to find the matching profile + profileStub[JsonKey(GuidKey)] = profileStub[JsonKey(UpdatesKey)]; + auto matchingProfile = _FindMatchingProfile(profileStub); + if (matchingProfile) + { + // We found a matching profile, create a child of it and put the modifications there + // (we add a new inheritance layer) + auto childImpl{ matchingProfile->CreateChild() }; + childImpl->LayerJson(profileStub); + + // replace parent in _profiles with child + _allProfiles.SetAt(_FindMatchingProfileIndex(matchingProfile->ToJson()).value(), *childImpl); + } + } + else + { + // This is a new profile, check that it meets our minimum requirements first + // (it must have at least a name) + if (profileStub.isMember(JsonKey(NameKey))) + { + auto newProfile = Profile::FromJson(profileStub); + // Make sure to give the new profile a source, then we add it to our list of profiles + // We don't make modifications to the user's settings file yet, that will happen when + // _AppendDynamicProfilesToUserSettings() is called later + newProfile->Source(source); + _allProfiles.Append(*newProfile); + } + } + } + } + + if (fullFile.isMember(JsonKey(SchemesKey))) + { + // Now we separately get each stub that adds a color scheme + for (const auto& schemeStub : fullFile[JsonKey(SchemesKey)]) + { + if (_FindMatchingColorScheme(schemeStub)) + { + // We do not allow modifications to existing color schemes + } + else + { + // This is a new color scheme, add it only if it specifies _all_ the fields + if (ColorScheme::ValidateColorScheme(schemeStub)) + { + const auto newScheme = ColorScheme::FromJson(schemeStub); + _globals->AddColorScheme(*newScheme); + } + } + } + } + } +} + // Method Description: // - Attempts to read the given data as a string of JSON and parse that JSON // into a Json::Value. @@ -400,6 +652,33 @@ void CascadiaSettings::_LoadDynamicProfiles() // - void CascadiaSettings::_ParseJsonString(std::string_view fileData, const bool isDefaultSettings) { + // Parse the json data into either our defaults or user settings. We'll keep + // these original json values around for later, in case we need to parse + // their raw contents again. + Json::Value& root = isDefaultSettings ? _defaultSettings : _userSettings; + + root = _ParseUtf8JsonString(fileData); + + // If this is the user settings, also store away the original settings + // string. We'll need to keep it around so we can modify it without + // re-serializing their settings. + if (!isDefaultSettings) + { + _userSettingsString = fileData; + } +} + +// Method Description: +// - Attempts to read the given data as a string of JSON and parse that JSON +// into a Json::Value +// - Will ignore leading UTF-8 BOMs +// Arguments: +// - fileData: the string to parse as JSON data +// Return value: +// - the parsed json value +Json::Value CascadiaSettings::_ParseUtf8JsonString(std::string_view fileData) +{ + Json::Value result; // Ignore UTF-8 BOM auto actualDataStart = fileData.data(); const auto actualDataEnd = fileData.data() + fileData.size(); @@ -411,25 +690,14 @@ void CascadiaSettings::_ParseJsonString(std::string_view fileData, const bool is std::string errs; // This string will receive any error text from failing to parse. std::unique_ptr reader{ Json::CharReaderBuilder::CharReaderBuilder().newCharReader() }; - // Parse the json data into either our defaults or user settings. We'll keep - // these original json values around for later, in case we need to parse - // their raw contents again. - Json::Value& root = isDefaultSettings ? _defaultSettings : _userSettings; // `parse` will return false if it fails. - if (!reader->parse(actualDataStart, actualDataEnd, &root, &errs)) + if (!reader->parse(actualDataStart, actualDataEnd, &result, &errs)) { // This will be caught by App::_TryLoadSettings, who will display // the text to the user. throw winrt::hresult_error(WEB_E_INVALID_JSON_STRING, winrt::to_hstring(errs)); } - - // If this is the user settings, also store away the original settings - // string. We'll need to keep it around so we can modify it without - // re-serializing their settings. - if (!isDefaultSettings) - { - _userSettingsString = fileData; - } + return result; } // Method Description: @@ -535,6 +803,7 @@ bool CascadiaSettings::_AppendDynamicProfilesToUserSettings() // changes to re-create this profile. const auto profileImpl = winrt::get_self(profile); const auto diff = profileImpl->GenerateStub(); + auto profileSerialization = Json::writeString(wbuilder, diff); // Add the user's indent to the start of each line diff --git a/src/cascadia/TerminalSettingsModel/ColorScheme.cpp b/src/cascadia/TerminalSettingsModel/ColorScheme.cpp index 2b748275945..8f86297e0d8 100644 --- a/src/cascadia/TerminalSettingsModel/ColorScheme.cpp +++ b/src/cascadia/TerminalSettingsModel/ColorScheme.cpp @@ -173,6 +173,29 @@ void ColorScheme::SetColorTableEntry(uint8_t index, const winrt::Windows::UI::Co _table[index] = value; } +// Method Description: +// - Validates a given color scheme +// - A color scheme is valid if it has a name and defines all the colors +// Arguments: +// - The color scheme to validate +// Return Value: +// - true if the scheme is valid, false otherwise +bool ColorScheme::ValidateColorScheme(const Json::Value& scheme) +{ + for (const auto& key : TableColors) + { + if (!scheme.isMember(JsonKey(key))) + { + return false; + } + } + if (!scheme.isMember(JsonKey(NameKey))) + { + return false; + } + return true; +} + // Method Description: // - Parse the name from the JSON representation of a ColorScheme. // Arguments: diff --git a/src/cascadia/TerminalSettingsModel/ColorScheme.h b/src/cascadia/TerminalSettingsModel/ColorScheme.h index 6f0bdb50240..d62f2a6ef68 100644 --- a/src/cascadia/TerminalSettingsModel/ColorScheme.h +++ b/src/cascadia/TerminalSettingsModel/ColorScheme.h @@ -48,6 +48,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation com_array Table() const noexcept; void SetColorTableEntry(uint8_t index, const winrt::Windows::UI::Color& value) noexcept; + static bool ValidateColorScheme(const Json::Value& scheme); + GETSET_PROPERTY(winrt::hstring, Name); GETSET_COLORPROPERTY(Foreground); // defined in constructor GETSET_COLORPROPERTY(Background); // defined in constructor diff --git a/src/cascadia/TerminalSettingsModel/pch.h b/src/cascadia/TerminalSettingsModel/pch.h index 8af40e6b66a..3c2a685e41b 100644 --- a/src/cascadia/TerminalSettingsModel/pch.h +++ b/src/cascadia/TerminalSettingsModel/pch.h @@ -30,11 +30,13 @@ #include #include +#include #include #include #include #include #include +#include #include