diff --git a/.github/actions/spell-check/dictionary/apis.txt b/.github/actions/spell-check/dictionary/apis.txt index 4c94cd575c2..a0564d98539 100644 --- a/.github/actions/spell-check/dictionary/apis.txt +++ b/.github/actions/spell-check/dictionary/apis.txt @@ -33,6 +33,7 @@ IObject IStorage LCID llabs +localtime lround LSHIFT msappx diff --git a/.github/actions/spell-check/dictionary/dictionary.txt b/.github/actions/spell-check/dictionary/dictionary.txt index d15b22d859d..beddeeec9c0 100644 --- a/.github/actions/spell-check/dictionary/dictionary.txt +++ b/.github/actions/spell-check/dictionary/dictionary.txt @@ -418872,6 +418872,7 @@ time-shrouded time-space time-spirit timestamp +timestamped timestamps timet time-table diff --git a/src/cascadia/LocalTests_SettingsModel/DeserializationTests.cpp b/src/cascadia/LocalTests_SettingsModel/DeserializationTests.cpp index f99a9531bd8..37ccb68a3be 100644 --- a/src/cascadia/LocalTests_SettingsModel/DeserializationTests.cpp +++ b/src/cascadia/LocalTests_SettingsModel/DeserializationTests.cpp @@ -884,7 +884,7 @@ namespace SettingsModelLocalTests VERIFY_ARE_EQUAL(0u, settings->_warnings.Size()); VERIFY_ARE_EQUAL(2u, settings->_allProfiles.Size()); VERIFY_IS_TRUE(settings->_allProfiles.GetAt(0).HasGuid()); - VERIFY_IS_TRUE(settings->_allProfiles.GetAt(1).HasGuid()); + VERIFY_IS_FALSE(settings->_allProfiles.GetAt(1).HasGuid()); } void DeserializationTests::TestReorderWithNullGuids() @@ -935,7 +935,7 @@ namespace SettingsModelLocalTests VERIFY_ARE_EQUAL(0u, settings->_warnings.Size()); VERIFY_ARE_EQUAL(4u, settings->_allProfiles.Size()); VERIFY_IS_TRUE(settings->_allProfiles.GetAt(0).HasGuid()); - VERIFY_IS_TRUE(settings->_allProfiles.GetAt(1).HasGuid()); + VERIFY_IS_FALSE(settings->_allProfiles.GetAt(1).HasGuid()); VERIFY_IS_TRUE(settings->_allProfiles.GetAt(2).HasGuid()); VERIFY_IS_TRUE(settings->_allProfiles.GetAt(3).HasGuid()); VERIFY_ARE_EQUAL(L"profile0", settings->_allProfiles.GetAt(0).Name()); @@ -1036,7 +1036,7 @@ namespace SettingsModelLocalTests VERIFY_ARE_EQUAL(0u, settings->_warnings.Size()); VERIFY_ARE_EQUAL(4u, settings->_allProfiles.Size()); VERIFY_IS_TRUE(settings->_allProfiles.GetAt(0).HasGuid()); - VERIFY_IS_TRUE(settings->_allProfiles.GetAt(1).HasGuid()); + VERIFY_IS_FALSE(settings->_allProfiles.GetAt(1).HasGuid()); VERIFY_IS_TRUE(settings->_allProfiles.GetAt(2).HasGuid()); VERIFY_IS_TRUE(settings->_allProfiles.GetAt(3).HasGuid()); VERIFY_ARE_EQUAL(L"Command Prompt", settings->_allProfiles.GetAt(0).Name()); @@ -1190,8 +1190,8 @@ namespace SettingsModelLocalTests VERIFY_ARE_EQUAL(5u, settings->_allProfiles.Size()); VERIFY_IS_TRUE(settings->_allProfiles.GetAt(0).HasGuid()); - VERIFY_IS_TRUE(settings->_allProfiles.GetAt(1).HasGuid()); - VERIFY_IS_TRUE(settings->_allProfiles.GetAt(2).HasGuid()); + VERIFY_IS_FALSE(settings->_allProfiles.GetAt(1).HasGuid()); + VERIFY_IS_FALSE(settings->_allProfiles.GetAt(2).HasGuid()); VERIFY_IS_TRUE(settings->_allProfiles.GetAt(3).HasGuid()); VERIFY_IS_TRUE(settings->_allProfiles.GetAt(4).HasGuid()); VERIFY_ARE_EQUAL(L"ThisProfileIsGood", settings->_allProfiles.GetAt(0).Name()); diff --git a/src/cascadia/LocalTests_SettingsModel/JsonTestClass.h b/src/cascadia/LocalTests_SettingsModel/JsonTestClass.h index a66b9f2af58..5cb0bf3f56b 100644 --- a/src/cascadia/LocalTests_SettingsModel/JsonTestClass.h +++ b/src/cascadia/LocalTests_SettingsModel/JsonTestClass.h @@ -22,6 +22,12 @@ class JsonTestClass { _reader = std::unique_ptr(Json::CharReaderBuilder::CharReaderBuilder().newCharReader()); }; + + void InitializeJsonWriter() + { + _writer = std::unique_ptr(Json::StreamWriterBuilder::StreamWriterBuilder().newStreamWriter()); + } + Json::Value VerifyParseSucceeded(std::string content) { Json::Value root; @@ -31,6 +37,14 @@ class JsonTestClass return root; }; + std::string toString(const Json::Value& json) + { + std::stringstream s; + _writer->write(json, &s); + return s.str(); + } + protected: std::unique_ptr _reader; + std::unique_ptr _writer; }; diff --git a/src/cascadia/LocalTests_SettingsModel/SerializationTests.cpp b/src/cascadia/LocalTests_SettingsModel/SerializationTests.cpp new file mode 100644 index 00000000000..60491de9cbf --- /dev/null +++ b/src/cascadia/LocalTests_SettingsModel/SerializationTests.cpp @@ -0,0 +1,295 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" + +#include "../TerminalSettingsModel/ColorScheme.h" +#include "../TerminalSettingsModel/CascadiaSettings.h" +#include "JsonTestClass.h" +#include "TestUtils.h" +#include +#include "../ut_app/TestDynamicProfileGenerator.h" + +using namespace Microsoft::Console; +using namespace WEX::Logging; +using namespace WEX::TestExecution; +using namespace WEX::Common; +using namespace winrt::Microsoft::Terminal::Settings::Model; +using namespace winrt::Microsoft::Terminal::TerminalControl; + +namespace SettingsModelLocalTests +{ + // TODO:microsoft/terminal#3838: + // Unfortunately, these tests _WILL NOT_ work in our CI. We're waiting for + // an updated TAEF that will let us install framework packages when the test + // package is deployed. Until then, these tests won't deploy in CI. + + class SerializationTests : public JsonTestClass + { + // Use a custom AppxManifest to ensure that we can activate winrt types + // from our test. This property will tell taef to manually use this as + // the AppxManifest for this test class. + // This does not yet work for anything XAML-y. See TabTests.cpp for more + // details on that. + BEGIN_TEST_CLASS(SerializationTests) + TEST_CLASS_PROPERTY(L"RunAs", L"UAP") + TEST_CLASS_PROPERTY(L"UAP:AppXManifest", L"TestHostAppXManifest.xml") + END_TEST_CLASS() + + TEST_METHOD(GlobalSettings); + TEST_METHOD(Profile); + TEST_METHOD(ColorScheme); + TEST_METHOD(CascadiaSettings); + + TEST_CLASS_SETUP(ClassSetup) + { + InitializeJsonReader(); + InitializeJsonWriter(); + return true; + } + + private: + // Method Description: + // - deserializes and reserializes a json string representing a settings object model of type T + // - verifies that the generated json string matches the provided one + // Template Types: + // - : The type of Settings Model object to generate (must be impl type) + // Arguments: + // - jsonString - JSON string we're performing the test on + // Return Value: + // - the JsonObject representing this instance + template + void RoundtripTest(const std::string& jsonString) + { + const auto json{ VerifyParseSucceeded(jsonString) }; + const auto settings{ T::FromJson(json) }; + const auto result{ settings->ToJson() }; + + // Compare toString(json) instead of jsonString here. + // The toString writes the json out alphabetically. + // This trick allows jsonString to _not_ have to be + // written alphabetically. + VERIFY_ARE_EQUAL(toString(json), toString(result)); + } + }; + + void SerializationTests::GlobalSettings() + { + const std::string globalsString{ R"( + { + "defaultProfile": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}", + + "initialRows": 30, + "initialCols": 120, + "initialPosition": ",", + "launchMode": "default", + "alwaysOnTop": false, + + "copyOnSelect": false, + "copyFormatting": "all", + "wordDelimiters": " /\\()\"'-.,:;<>~!@#$%^&*|+=[]{}~?\u2502", + + "alwaysShowTabs": true, + "showTabsInTitlebar": true, + "showTerminalTitleInTitlebar": true, + "tabWidthMode": "equal", + "tabSwitcherMode": "mru", + + "startOnUserLogin": false, + "theme": "system", + "snapToGridOnResize": true, + "disableAnimations": false, + + "confirmCloseAllTabs": true, + "largePasteWarning": true, + "multiLinePasteWarning": true, + + "experimental.input.forceVT": false, + "experimental.rendering.forceFullRepaint": false, + "experimental.rendering.software": false + })" }; + + const std::string smallGlobalsString{ R"( + { + "defaultProfile": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}" + })" }; + + RoundtripTest(globalsString); + RoundtripTest(smallGlobalsString); + } + + void SerializationTests::Profile() + { + const std::string profileString{ R"( + { + "name": "Windows PowerShell", + "guid": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}", + + "commandline": "%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", + "startingDirectory": "%USERPROFILE%", + + "icon": "ms-appx:///ProfileIcons/{61c54bbd-c2c6-5271-96e7-009a87ff44bf}.png", + "hidden": false, + + "tabTitle": "Cool Tab", + "suppressApplicationTitle": false, + + "fontFace": "Cascadia Mono", + "fontSize": 12, + "fontWeight": "normal", + "padding": "8, 8, 8, 8", + "antialiasingMode": "grayscale", + + "cursorShape": "bar", + "cursorColor": "#CCBBAA", + "cursorHeight": 10, + + "altGrAliasing": true, + + "colorScheme": "Campbell", + "tabColor": "#0C0C0C", + "foreground": "#AABBCC", + "background": "#BBCCAA", + "selectionBackground": "#CCAABB", + + "useAcrylic": false, + "acrylicOpacity": 0.5, + + "backgroundImage": "made_you_look.jpeg", + "backgroundImageStretchMode": "uniformToFill", + "backgroundImageAlignment": "center", + "backgroundImageOpacity": 1.0, + + "scrollbarState": "visible", + "snapOnInput": true, + "historySize": 9001, + + "closeOnExit": "graceful", + "experimental.retroTerminalEffect": false + })" }; + + const std::string smallProfileString{ R"( + { + "name": "Custom Profile" + })" }; + + // Setting "tabColor" to null tests two things: + // - null should count as an explicit user-set value, not falling back to the parent's value + // - null should be acceptable even though we're working with colors + const std::string weirdProfileString{ R"( + { + "name": "Weird Profile", + "tabColor": null, + "foreground": null, + "source": "local" + })" }; + + RoundtripTest(profileString); + RoundtripTest(smallProfileString); + RoundtripTest(weirdProfileString); + } + + void SerializationTests::ColorScheme() + { + const std::string schemeString{ R"({ + "name": "Campbell", + + "cursorColor": "#FFFFFF", + "selectionBackground": "#131313", + + "background": "#0C0C0C", + "foreground": "#F2F2F2", + + "black": "#0C0C0C", + "blue": "#0037DA", + "cyan": "#3A96DD", + "green": "#13A10E", + "purple": "#881798", + "red": "#C50F1F", + "white": "#CCCCCC", + "yellow": "#C19C00", + "brightBlack": "#767676", + "brightBlue": "#3B78FF", + "brightCyan": "#61D6D6", + "brightGreen": "#16C60C", + "brightPurple": "#B4009E", + "brightRed": "#E74856", + "brightWhite": "#F2F2F2", + "brightYellow": "#F9F1A5" + })" }; + + RoundtripTest(schemeString); + } + + void SerializationTests::CascadiaSettings() + { + const std::string settingsString{ R"({ + "$schema": "https://aka.ms/terminal-profiles-schema", + "defaultProfile": "{61c54bbd-1111-5271-96e7-009a87ff44bf}", + + "profiles": { + "defaults": { + "fontFace": "Zamora Code" + }, + "list": [ + { + "fontFace": "Cascadia Code", + "guid": "{61c54bbd-1111-5271-96e7-009a87ff44bf}", + "name": "HowettShell" + }, + { + "hidden": true, + "name": "BhojwaniShell" + }, + { + "antialiasingMode": "aliased", + "name": "NiksaShell" + } + ] + }, + "schemes": [ + { + "name": "Cinnamon Roll", + + "cursorColor": "#FFFFFD", + "selectionBackground": "#FFFFFF", + + "background": "#3C0315", + "foreground": "#FFFFFD", + + "black": "#282A2E", + "blue": "#0170C5", + "cyan": "#3F8D83", + "green": "#76AB23", + "purple": "#7D498F", + "red": "#BD0940", + "white": "#FFFFFD", + "yellow": "#E0DE48", + "brightBlack": "#676E7A", + "brightBlue": "#5C98C5", + "brightCyan": "#8ABEB7", + "brightGreen": "#B5D680", + "brightPurple": "#AC79BB", + "brightRed": "#BD6D85", + "brightWhite": "#FFFFFD", + "brightYellow": "#FFFD76" + } + ], + "actions": [ + {"command": { "action": "renameTab","input": "Liang Tab" },"keys": "ctrl+t" } + ], + "keybindings": [ + { "command": { "action": "sendInput","input": "VT Griese Mode" },"keys": "ctrl+k" } + ] + })" }; + + auto settings{ winrt::make_self(false) }; + settings->_ParseJsonString(settingsString, false); + settings->_ApplyDefaultsFromUserSettings(); + settings->LayerJson(settings->_userSettings); + settings->_ValidateSettings(); + + const auto result{ settings->ToJson() }; + VERIFY_ARE_EQUAL(toString(settings->_userSettings), toString(result)); + } +} diff --git a/src/cascadia/LocalTests_SettingsModel/SettingsModel.LocalTests.vcxproj b/src/cascadia/LocalTests_SettingsModel/SettingsModel.LocalTests.vcxproj index d97c3ab272f..547ea04dadb 100644 --- a/src/cascadia/LocalTests_SettingsModel/SettingsModel.LocalTests.vcxproj +++ b/src/cascadia/LocalTests_SettingsModel/SettingsModel.LocalTests.vcxproj @@ -39,6 +39,7 @@ + Create diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp index 35ee46c0a13..17010823b94 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp @@ -247,11 +247,6 @@ void CascadiaSettings::_ValidateSettings() // Make sure to check that profiles exists at all first and foremost: _ValidateProfilesExist(); - // Verify all profiles actually had a GUID specified, otherwise generate a - // GUID for them. Make sure to do this before de-duping profiles and - // checking that the default profile is set. - _ValidateProfilesHaveGuid(); - // Re-order profiles so that all profiles from the user's settings appear // before profiles that _weren't_ in the user profiles. _ReorderProfilesToMatchUserSettingsOrder(); @@ -308,19 +303,6 @@ void CascadiaSettings::_ValidateProfilesExist() } } -// Method Description: -// - Walks through each profile, and ensures that they had a GUID set at some -// point. If the profile did _not_ have a GUID ever set for it, generate a -// temporary runtime GUID for it. This validation does not add any warnings. -void CascadiaSettings::_ValidateProfilesHaveGuid() -{ - for (auto profile : _allProfiles) - { - auto profileImpl = winrt::get_self(profile); - profileImpl->GenerateGuidIfNecessary(); - } -} - // Method Description: // - Resolves the "defaultProfile", which can be a profile name, to a GUID // and stores it back to the globals. @@ -509,7 +491,8 @@ void CascadiaSettings::_ValidateAllSchemesExist() const auto schemeName = profile.ColorSchemeName(); if (!_globals->ColorSchemes().HasKey(schemeName)) { - profile.ColorSchemeName({ L"Campbell" }); + // Clear the user set color scheme. We'll just fallback instead. + profile.ClearColorSchemeName(); foundInvalidScheme = true; } } @@ -727,7 +710,8 @@ void CascadiaSettings::_ValidateKeybindings() // we find any invalid background images. void CascadiaSettings::_ValidateNoGlobalsKey() { - if (auto oldGlobalsProperty{ _userSettings["globals"] }) + // use isMember here. If you use [], you're actually injecting "globals": null. + if (_userSettings.isMember("globals")) { _warnings.Append(SettingsLoadWarnings::LegacyGlobalsProperty); } diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettings.h b/src/cascadia/TerminalSettingsModel/CascadiaSettings.h index 45e1c0d8cdf..8151e32e41d 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettings.h +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettings.h @@ -29,6 +29,7 @@ Author(s): // fwdecl unittest classes namespace SettingsModelLocalTests { + class SerializationTests; class DeserializationTests; class ProfileTests; class ColorSchemeTests; @@ -74,6 +75,9 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation static com_ptr FromJson(const Json::Value& json); void LayerJson(const Json::Value& json); + void WriteSettingsToDisk() const; + Json::Value ToJson() const; + static hstring SettingsPath(); static hstring DefaultSettingsPath(); @@ -124,7 +128,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation void _LoadDynamicProfiles(); static bool _IsPackaged(); - static void _WriteSettings(const std::string_view content); + static void _WriteSettings(std::string_view content, const hstring filepath); static std::optional _ReadUserSettings(); static std::optional _ReadFile(HANDLE hFile); @@ -133,7 +137,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation void _ValidateSettings(); void _ValidateProfilesExist(); - void _ValidateProfilesHaveGuid(); void _ValidateDefaultProfileExists(); void _ValidateNoDuplicateProfiles(); void _ResolveDefaultProfile(); @@ -144,6 +147,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation void _ValidateKeybindings(); void _ValidateNoGlobalsKey(); + friend class SettingsModelLocalTests::SerializationTests; friend class SettingsModelLocalTests::DeserializationTests; friend class SettingsModelLocalTests::ProfileTests; friend class SettingsModelLocalTests::ColorSchemeTests; diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettings.idl b/src/cascadia/TerminalSettingsModel/CascadiaSettings.idl index 13c8fb258cc..346287413f9 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettings.idl +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettings.idl @@ -11,6 +11,8 @@ namespace Microsoft.Terminal.Settings.Model CascadiaSettings(String json); CascadiaSettings Copy(); + void WriteSettingsToDisk(); + static CascadiaSettings LoadDefaults(); static CascadiaSettings LoadAll(); static CascadiaSettings LoadUniversal(); diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp index d786d400df2..87e003aab80 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp @@ -9,6 +9,7 @@ #include "JsonUtils.h" #include #include +#include // defaults.h is a file containing the default json settings in a std::string_view #include "defaults.h" @@ -28,10 +29,12 @@ static constexpr std::wstring_view UnpackagedSettingsFolderName{ L"Microsoft\\Wi static constexpr std::wstring_view DefaultsFilename{ L"defaults.json" }; static constexpr std::string_view SchemaKey{ "$schema" }; +static constexpr std::string_view SchemaValue{ "https://aka.ms/terminal-profiles-schema" }; 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 KeybindingsKey{ "keybindings" }; +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 DisabledProfileSourcesKey{ "disabledProfileSources" }; @@ -188,7 +191,7 @@ winrt::Microsoft::Terminal::Settings::Model::CascadiaSettings CascadiaSettings:: try { - _WriteSettings(resultPtr->_userSettingsString); + _WriteSettings(resultPtr->_userSettingsString, CascadiaSettings::SettingsPath()); } catch (...) { @@ -221,7 +224,7 @@ winrt::Microsoft::Terminal::Settings::Model::CascadiaSettings CascadiaSettings:: } // If the user had keybinding settings preferences, we want to learn from them to make better defaults - auto userKeybindings = resultPtr->_userSettings[JsonKey(KeybindingsKey)]; + auto userKeybindings = resultPtr->_userSettings[JsonKey(LegacyKeybindingsKey)]; if (!userKeybindings.empty()) { // If there are custom key bindings, let's understand what they are because maybe the defaults aren't good enough @@ -366,8 +369,6 @@ void CascadiaSettings::_LoadDynamicProfiles() auto profiles = generator->GenerateProfiles(); for (auto& profile : profiles) { - // If the profile did not have a GUID when it was generated, - // we'll synthesize a GUID for it in _ValidateProfilesHaveGuid profile.Source(generatorNamespace); _allProfiles.Append(profile); @@ -829,7 +830,7 @@ bool CascadiaSettings::_IsPackaged() } // Method Description: -// - Writes the given content in UTF-8 to our settings file using the Win32 APIS's. +// - Writes the given content in UTF-8 to a settings file using the Win32 APIS's. // Will overwrite any existing content in the file. // Arguments: // - content: the given string of content to write to the file. @@ -837,11 +838,9 @@ bool CascadiaSettings::_IsPackaged() // - // This can throw an exception if we fail to open the file for writing, or we // fail to write the file -void CascadiaSettings::_WriteSettings(const std::string_view content) +void CascadiaSettings::_WriteSettings(const std::string_view content, const hstring filepath) { - auto pathToSettingsFile{ CascadiaSettings::SettingsPath() }; - - wil::unique_hfile hOut{ CreateFileW(pathToSettingsFile.c_str(), + wil::unique_hfile hOut{ CreateFileW(filepath.c_str(), GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, @@ -1039,3 +1038,83 @@ const Json::Value& CascadiaSettings::_GetDisabledProfileSourcesJsonObject(const } return json[JsonKey(DisabledProfileSourcesKey)]; } + +// 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 +// Arguments: +// - +// Return Value: +// - +void CascadiaSettings::WriteSettingsToDisk() const +{ + const auto settingsPath{ CascadiaSettings::SettingsPath() }; + + try + { + // create a timestamped backup file + const auto clock{ std::chrono::system_clock() }; + const auto timeStamp{ clock.to_time_t(clock.now()) }; + const winrt::hstring backupSettingsPath{ fmt::format(L"{}.{:%Y-%m-%dT%H-%M-%S}.backup", settingsPath, fmt::localtime(timeStamp)) }; + _WriteSettings(_userSettingsString, backupSettingsPath); + } + CATCH_LOG(); + + // write current settings to current settings file + Json::StreamWriterBuilder wbuilder; + wbuilder.settings_["indentation"] = " "; + wbuilder.settings_["enableYAMLCompatibility"] = true; // suppress spaces around colons + + const auto styledString{ Json::writeString(wbuilder, ToJson()) }; + _WriteSettings(styledString, settingsPath); +} + +// 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 + // directly inject "globals" and "$schema" into here + Json::Value json{ _globals->ToJson() }; + JsonUtils::SetValueForKey(json, SchemaKey, JsonKey(SchemaValue)); + + // "profiles" will always be serialized as an object + Json::Value profiles{ Json::ValueType::objectValue }; + profiles[JsonKey(DefaultSettingsKey)] = _userDefaultProfileSettings ? _userDefaultProfileSettings->ToJson() : + Json::ValueType::objectValue; + Json::Value profilesList{ Json::ValueType::arrayValue }; + for (const auto& entry : _allProfiles) + { + const auto prof{ winrt::get_self(entry) }; + profilesList.append(prof->ToJson()); + } + profiles[JsonKey(ProfilesListKey)] = profilesList; + json[JsonKey(ProfilesKey)] = profiles; + + // TODO GH#8100: + // "schemes" will be an accumulation of _all_ the color schemes + // including all of the ones from defaults.json + Json::Value schemes{ Json::ValueType::arrayValue }; + for (const auto& entry : _globals->ColorSchemes()) + { + const auto scheme{ winrt::get_self(entry.Value()) }; + schemes.append(scheme->ToJson()); + } + json[JsonKey(SchemesKey)] = schemes; + + // "actions"/"keybindings" will be whatever blob we had in the file + if (_userSettings.isMember(JsonKey(LegacyKeybindingsKey))) + { + json[JsonKey(LegacyKeybindingsKey)] = _userSettings[JsonKey(LegacyKeybindingsKey)]; + } + if (_userSettings.isMember(JsonKey(ActionsKey))) + { + json[JsonKey(ActionsKey)] = _userSettings[JsonKey(ActionsKey)]; + } + + return json; +} diff --git a/src/cascadia/TerminalSettingsModel/ColorScheme.cpp b/src/cascadia/TerminalSettingsModel/ColorScheme.cpp index 1daf81f6671..0bbb409563a 100644 --- a/src/cascadia/TerminalSettingsModel/ColorScheme.cpp +++ b/src/cascadia/TerminalSettingsModel/ColorScheme.cpp @@ -129,8 +129,8 @@ void ColorScheme::LayerJson(const Json::Value& json) // Arguments: // - // Return Value: -// -Json::Value ColorScheme::ToJson() +// - the JsonObject representing this instance +Json::Value ColorScheme::ToJson() const { Json::Value json{ Json::ValueType::objectValue }; diff --git a/src/cascadia/TerminalSettingsModel/ColorScheme.h b/src/cascadia/TerminalSettingsModel/ColorScheme.h index d5abd4ac813..905e1837ea2 100644 --- a/src/cascadia/TerminalSettingsModel/ColorScheme.h +++ b/src/cascadia/TerminalSettingsModel/ColorScheme.h @@ -40,7 +40,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation bool ShouldBeLayered(const Json::Value& json) const; void LayerJson(const Json::Value& json); - Json::Value ToJson(); + Json::Value ToJson() const; static std::optional GetNameFromJson(const Json::Value& json); diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp index 45ccc938449..28a1f1aa529 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp @@ -53,12 +53,16 @@ static constexpr bool debugFeaturesDefault{ true }; static constexpr bool debugFeaturesDefault{ false }; #endif +bool GlobalAppSettings::_getDefaultDebugFeaturesValue() +{ + return debugFeaturesDefault; +} + GlobalAppSettings::GlobalAppSettings() : _keymap{ winrt::make_self() }, _keybindingsWarnings{}, _validDefaultProfile{ false }, - _defaultProfile{}, - _DebugFeaturesEnabled{ debugFeaturesDefault } + _defaultProfile{} { _commands = winrt::single_threaded_map(); _colorSchemes = winrt::single_threaded_map(); @@ -347,3 +351,49 @@ winrt::Windows::Foundation::Collections::IMapView +// Return Value: +// - the JsonObject representing this instance +Json::Value GlobalAppSettings::ToJson() const +{ + Json::Value json{ Json::ValueType::objectValue }; + + // clang-format off + JsonUtils::SetValueForKey(json, DefaultProfileKey, _UnparsedDefaultProfile); + JsonUtils::SetValueForKey(json, AlwaysShowTabsKey, _AlwaysShowTabs); + JsonUtils::SetValueForKey(json, ConfirmCloseAllKey, _ConfirmCloseAllTabs); + JsonUtils::SetValueForKey(json, InitialRowsKey, _InitialRows); + JsonUtils::SetValueForKey(json, InitialColsKey, _InitialCols); + JsonUtils::SetValueForKey(json, InitialPositionKey, _InitialPosition); + JsonUtils::SetValueForKey(json, ShowTitleInTitlebarKey, _ShowTitleInTitlebar); + JsonUtils::SetValueForKey(json, ShowTabsInTitlebarKey, _ShowTabsInTitlebar); + JsonUtils::SetValueForKey(json, WordDelimitersKey, _WordDelimiters); + JsonUtils::SetValueForKey(json, CopyOnSelectKey, _CopyOnSelect); + JsonUtils::SetValueForKey(json, CopyFormattingKey, _CopyFormatting); + JsonUtils::SetValueForKey(json, WarnAboutLargePasteKey, _WarnAboutLargePaste); + JsonUtils::SetValueForKey(json, WarnAboutMultiLinePasteKey, _WarnAboutMultiLinePaste); + JsonUtils::SetValueForKey(json, LaunchModeKey, _LaunchMode); + JsonUtils::SetValueForKey(json, ThemeKey, _Theme); + JsonUtils::SetValueForKey(json, TabWidthModeKey, _TabWidthMode); + JsonUtils::SetValueForKey(json, SnapToGridOnResizeKey, _SnapToGridOnResize); + JsonUtils::SetValueForKey(json, DebugFeaturesKey, _DebugFeaturesEnabled); + JsonUtils::SetValueForKey(json, ForceFullRepaintRenderingKey, _ForceFullRepaintRendering); + JsonUtils::SetValueForKey(json, SoftwareRenderingKey, _SoftwareRendering); + JsonUtils::SetValueForKey(json, ForceVTInputKey, _ForceVTInput); + JsonUtils::SetValueForKey(json, EnableStartupTaskKey, _StartOnUserLogin); + JsonUtils::SetValueForKey(json, AlwaysOnTopKey, _AlwaysOnTop); + JsonUtils::SetValueForKey(json, TabSwitcherModeKey, _TabSwitcherMode); + JsonUtils::SetValueForKey(json, DisableAnimationsKey, _DisableAnimations); + // clang-format on + + // TODO GH#8100: keymap needs to be serialized here + // For deserialization, we iterate over each action in the Json and interpret it as a keybinding, then as a command. + // Converting this back to JSON is a problem because we have no way to know if a Command and Keybinding come from + // the same entry. + + return json; +} diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h index 87f1bfce21b..b4b5f6f4c76 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h @@ -46,6 +46,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation static com_ptr FromJson(const Json::Value& json); void LayerJson(const Json::Value& json); + Json::Value ToJson() const; + std::vector KeybindingsWarnings() const; Windows::Foundation::Collections::IMapView Commands() noexcept; @@ -78,7 +80,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation GETSET_SETTING(bool, ForceFullRepaintRendering, false); GETSET_SETTING(bool, SoftwareRendering, false); GETSET_SETTING(bool, ForceVTInput, false); - GETSET_SETTING(bool, DebugFeaturesEnabled); // default value set in constructor + GETSET_SETTING(bool, DebugFeaturesEnabled, _getDefaultDebugFeaturesValue()); GETSET_SETTING(bool, StartOnUserLogin, false); GETSET_SETTING(bool, AlwaysOnTop, false); GETSET_SETTING(Model::TabSwitcherMode, TabSwitcherMode, Model::TabSwitcherMode::MostRecentlyUsed); @@ -96,6 +98,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation Windows::Foundation::Collections::IMap _commands; std::optional _getUnparsedDefaultProfileImpl() const; + static bool _getDefaultDebugFeaturesValue(); friend class SettingsModelLocalTests::DeserializationTests; friend class SettingsModelLocalTests::ColorSchemeTests; diff --git a/src/cascadia/TerminalSettingsModel/JsonUtils.h b/src/cascadia/TerminalSettingsModel/JsonUtils.h index 13b77637a5d..37b24d84579 100644 --- a/src/cascadia/TerminalSettingsModel/JsonUtils.h +++ b/src/cascadia/TerminalSettingsModel/JsonUtils.h @@ -88,6 +88,13 @@ namespace Microsoft::Terminal::Settings::Model::JsonUtils static constexpr auto&& Value(const ::winrt::Windows::Foundation::IReference& o) { return o.Value(); } }; + class SerializationError : public std::runtime_error + { + public: + SerializationError() : + runtime_error("failed to serialize") {} + }; + class DeserializationError : public std::runtime_error { public: @@ -517,7 +524,7 @@ namespace Microsoft::Terminal::Settings::Model::JsonUtils return { pair.first.data() }; } } - FAIL_FAST(); + throw SerializationError{}; } std::string TypeDescription() const diff --git a/src/cascadia/TerminalSettingsModel/Profile.cpp b/src/cascadia/TerminalSettingsModel/Profile.cpp index 7017f15e255..ca0bf1fdddf 100644 --- a/src/cascadia/TerminalSettingsModel/Profile.cpp +++ b/src/cascadia/TerminalSettingsModel/Profile.cpp @@ -291,6 +291,7 @@ void Profile::LayerJson(const Json::Value& json) JsonUtils::GetValueForKey(json, NameKey, _Name); JsonUtils::GetValueForKey(json, GuidKey, _Guid); JsonUtils::GetValueForKey(json, HiddenKey, _Hidden); + JsonUtils::GetValueForKey(json, SourceKey, _Source); // Core Settings JsonUtils::GetValueForKey(json, ForegroundKey, _Foreground); @@ -417,27 +418,6 @@ std::wstring Profile::EvaluateStartingDirectory(const std::wstring& directory) } } -// Method Description: -// - If this profile never had a GUID set for it, generate a runtime GUID for -// the profile. If a profile had their guid manually set to {0}, this method -// will _not_ change the profile's GUID. -void Profile::GenerateGuidIfNecessary() noexcept -{ - if (!_getGuidImpl().has_value()) - { - // Always use the name to generate the temporary GUID. That way, across - // reloads, we'll generate the same static GUID. - _Guid = Profile::_GenerateGuidForProfile(Name(), Source()); - - TraceLoggingWrite( - g_hSettingsModelProvider, - "SynthesizedGuidForProfile", - TraceLoggingDescription("Event emitted when a profile is deserialized without a GUID"), - TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), - TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance)); - } -} - // Function Description: // - Returns true if the given JSON object represents a dynamic profile object. // If it is a dynamic profile object, we should make sure to only layer the @@ -540,3 +520,63 @@ void Profile::BackgroundImageVerticalAlignment(const VerticalAlignment& value) n } } #pragma endregion + +// Method Description: +// - Create a new serialized JsonObject from an instance of this class +// Arguments: +// - +// Return Value: +// - the JsonObject representing this instance +Json::Value Profile::ToJson() const +{ + Json::Value json{ Json::ValueType::objectValue }; + + // Profile-specific Settings + JsonUtils::SetValueForKey(json, NameKey, _Name); + JsonUtils::SetValueForKey(json, GuidKey, _Guid); + JsonUtils::SetValueForKey(json, HiddenKey, _Hidden); + JsonUtils::SetValueForKey(json, SourceKey, _Source); + + // Core Settings + JsonUtils::SetValueForKey(json, ForegroundKey, _Foreground); + JsonUtils::SetValueForKey(json, BackgroundKey, _Background); + JsonUtils::SetValueForKey(json, SelectionBackgroundKey, _SelectionBackground); + JsonUtils::SetValueForKey(json, CursorColorKey, _CursorColor); + JsonUtils::SetValueForKey(json, ColorSchemeKey, _ColorSchemeName); + + // TODO:MSFT:20642297 - Use a sentinel value (-1) for "Infinite scrollback" + JsonUtils::SetValueForKey(json, HistorySizeKey, _HistorySize); + JsonUtils::SetValueForKey(json, SnapOnInputKey, _SnapOnInput); + JsonUtils::SetValueForKey(json, AltGrAliasingKey, _AltGrAliasing); + JsonUtils::SetValueForKey(json, CursorHeightKey, _CursorHeight); + JsonUtils::SetValueForKey(json, CursorShapeKey, _CursorShape); + JsonUtils::SetValueForKey(json, TabTitleKey, _TabTitle); + + // Control Settings + JsonUtils::SetValueForKey(json, FontWeightKey, _FontWeight); + JsonUtils::SetValueForKey(json, ConnectionTypeKey, _ConnectionType); + JsonUtils::SetValueForKey(json, CommandlineKey, _Commandline); + JsonUtils::SetValueForKey(json, FontFaceKey, _FontFace); + JsonUtils::SetValueForKey(json, FontSizeKey, _FontSize); + JsonUtils::SetValueForKey(json, AcrylicTransparencyKey, _AcrylicOpacity); + JsonUtils::SetValueForKey(json, UseAcrylicKey, _UseAcrylic); + JsonUtils::SetValueForKey(json, SuppressApplicationTitleKey, _SuppressApplicationTitle); + JsonUtils::SetValueForKey(json, CloseOnExitKey, _CloseOnExit); + + // PermissiveStringConverter is unnecessary for serialization + JsonUtils::SetValueForKey(json, PaddingKey, _Padding); + + JsonUtils::SetValueForKey(json, ScrollbarStateKey, _ScrollState); + JsonUtils::SetValueForKey(json, StartingDirectoryKey, _StartingDirectory); + JsonUtils::SetValueForKey(json, IconKey, _Icon); + JsonUtils::SetValueForKey(json, BackgroundImageKey, _BackgroundImagePath); + JsonUtils::SetValueForKey(json, BackgroundImageOpacityKey, _BackgroundImageOpacity); + JsonUtils::SetValueForKey(json, BackgroundImageStretchModeKey, _BackgroundImageStretchMode); + JsonUtils::SetValueForKey(json, BackgroundImageAlignmentKey, _BackgroundImageAlignment); + JsonUtils::SetValueForKey(json, RetroTerminalEffectKey, _RetroTerminalEffect); + JsonUtils::SetValueForKey(json, AntialiasingModeKey, _AntialiasingMode); + JsonUtils::SetValueForKey(json, TabColorKey, _TabColor); + JsonUtils::SetValueForKey(json, BellStyleKey, _BellStyle); + + return json; +} diff --git a/src/cascadia/TerminalSettingsModel/Profile.h b/src/cascadia/TerminalSettingsModel/Profile.h index 4a42c4108bf..c8041a99df2 100644 --- a/src/cascadia/TerminalSettingsModel/Profile.h +++ b/src/cascadia/TerminalSettingsModel/Profile.h @@ -55,10 +55,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation bool ShouldBeLayered(const Json::Value& json) const; void LayerJson(const Json::Value& json); static bool IsDynamicProfileObject(const Json::Value& json); + Json::Value ToJson() const; hstring EvaluatedStartingDirectory() const; hstring ExpandedBackgroundImagePath() const; - void GenerateGuidIfNecessary() noexcept; static guid GetGuidOrGenerateForJson(const Json::Value& json) noexcept; // BackgroundImageAlignment is 1 setting saved as 2 separate values diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h b/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h index 203473ec695..f8806a614ba 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h +++ b/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h @@ -154,6 +154,19 @@ struct ::Microsoft::Terminal::Settings::Model::JsonUtils::ConversionTrait<::winr return weight; } + Json::Value ToJson(const ::winrt::Windows::UI::Text::FontWeight& val) + { + const auto weight{ val.Weight }; + try + { + return BaseEnumMapper::ToJson(weight); + } + catch (SerializationError&) + { + return weight; + } + } + bool CanConvert(const Json::Value& json) { return BaseEnumMapper::CanConvert(json) || json.isUInt(); diff --git a/src/cascadia/ut_app/DynamicProfileTests.cpp b/src/cascadia/ut_app/DynamicProfileTests.cpp index 98ec7236197..c7d6daf6c53 100644 --- a/src/cascadia/ut_app/DynamicProfileTests.cpp +++ b/src/cascadia/ut_app/DynamicProfileTests.cpp @@ -110,8 +110,7 @@ namespace TerminalAppUnitTests void DynamicProfileTests::TestGenGuidsForProfiles() { - // We'll generate GUIDs during - // CascadiaSettings::_ValidateProfilesHaveGuid. We should make sure that + // We'll generate GUIDs in the Profile::Guid getter. We should make sure that // the GUID generated for a dynamic profile (with a source) is different // than that of a profile without a source. @@ -167,14 +166,6 @@ namespace TerminalAppUnitTests VERIFY_IS_FALSE(settings->_allProfiles.GetAt(4).HasGuid()); VERIFY_IS_FALSE(settings->_allProfiles.GetAt(4).Source().empty()); - settings->_ValidateProfilesHaveGuid(); - - VERIFY_IS_TRUE(settings->_allProfiles.GetAt(0).HasGuid()); - VERIFY_IS_TRUE(settings->_allProfiles.GetAt(1).HasGuid()); - VERIFY_IS_TRUE(settings->_allProfiles.GetAt(2).HasGuid()); - VERIFY_IS_TRUE(settings->_allProfiles.GetAt(3).HasGuid()); - VERIFY_IS_TRUE(settings->_allProfiles.GetAt(4).HasGuid()); - VERIFY_ARE_NOT_EQUAL(settings->_allProfiles.GetAt(0).Guid(), settings->_allProfiles.GetAt(1).Guid()); VERIFY_ARE_NOT_EQUAL(settings->_allProfiles.GetAt(0).Guid(), diff --git a/src/cascadia/ut_app/JsonTests.cpp b/src/cascadia/ut_app/JsonTests.cpp index c3d240f11c4..9111893b5ad 100644 --- a/src/cascadia/ut_app/JsonTests.cpp +++ b/src/cascadia/ut_app/JsonTests.cpp @@ -120,7 +120,7 @@ namespace TerminalAppUnitTests { // Parse some profiles without guids. We should NOT generate new guids // for them. If a profile doesn't have a GUID, we'll leave its _guid - // set to nullopt. CascadiaSettings::_ValidateProfilesHaveGuid will + // set to nullopt. The Profile::Guid() getter will // ensure all profiles have a GUID that's actually set. // The null guid _is_ a valid guid, so we won't re-generate that // guid. null is _not_ a valid guid, so we'll leave that nullopt