diff --git a/doc/user-docs/UsingJsonSettings.md b/doc/user-docs/UsingJsonSettings.md index 537bdd82bd7..93c4c46820f 100644 --- a/doc/user-docs/UsingJsonSettings.md +++ b/doc/user-docs/UsingJsonSettings.md @@ -1,12 +1,22 @@ # Editing Windows Terminal JSON Settings -One way (currently the only way) to configure Windows Terminal is by editing the profiles.json settings file. At -the time of writing you can open the settings file in your default editor by selecting -`Settings` from the WT pull down menu. +One way (currently the only way) to configure Windows Terminal is by editing the +`profiles.json` settings file. At the time of writing you can open the settings +file in your default editor by selecting `Settings` from the WT pull down menu. -The settings are stored in the file `$env:LocalAppData\Packages\Microsoft.WindowsTerminal_\RoamingState\profiles.json` +The settings are stored in the file `$env:LocalAppData\Packages\Microsoft.WindowsTerminal_\RoamingState\profiles.json`. -Details of specific settings can be found [here](../cascadia/SettingsSchema.md). A general introduction is provided below. +As of [#2515](https://github.com/microsoft/terminal/pull/2515), the settings are +split into _two_ files: a hardcoded `defaults.json`, and `profiles.json`, which +contains the user settings. Users should only be concerned with the contents of +the `profiles.json`, which contains their customizations. The `defaults.json` +file is only provided as a reference of what the default settings are. For more +details on how these two files work, see [Settings +Layering](#settings-layering). To view the default settings file, click on the +"Settings" button while holding the Alt key. + +Details of specific settings can be found [here](../cascadia/SettingsSchema.md). +A general introduction is provided below. The settings are grouped under four headings: @@ -17,12 +27,13 @@ The settings are grouped under four headings: ## Global Settings -These settings define startup defaults. +These settings define startup defaults, and application-wide settings that might +not affect a particular terminal instance. * Theme * Title Bar options * Initial size -* Default profile used when WT is started +* Default profile used when the Windows Terminal is started Example settings include @@ -31,10 +42,13 @@ Example settings include "initialCols" : 120, "initialRows" : 50, "requestedTheme" : "system", - "keybinding" : [] + "keybindings" : [] ... ``` +These global properties can exist either in the root json object, or in and +object under a root property `"globals"`. + ## Key Bindings This is an array of key chords and shortcuts to invoke various commands. @@ -43,10 +57,29 @@ Each command can have more than one key binding. NOTE: Key bindings is a subfield of the global settings and key bindings apply to all profiles in the same manner. +For example, here's a sample of the default keybindings: + +```json +{ + "keybindings": + [ + { "command": "closePane", "keys": ["ctrl+shift+w"] }, + { "command": "copy", "keys": ["ctrl+shift+c"] }, + { "command": "newTab", "keys": ["ctrl+shift+t"] }, + // etc. + ] +} + +``` + ## Profiles -A profile contains the settings applied when a new WT tab is opened. Each profile is identified by a GUID and contains -a number of other fields. +A profile contains the settings applied when a new WT tab is opened. Each +profile is identified by a GUID and contains a number of other fields. + +> 👉 **Note**: The `guid` property is the unique identifier for a profile. If +> multiple profiles all have the same `guid` value, you may see unexpected +> behavior. * Which command to execute on startup - this can include arguments. * Starting directory @@ -77,6 +110,14 @@ The profile GUID is used to reference the default profile in the global settings The values for background image stretch mode are documented [here](https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.media.stretch) +### Hiding a profile + +If you want to remove a profile from the list of profiles in the new tab +dropdown, but keep the profile around in your `profiles.json` file, you can add +the property `"hidden": true` to the profile's json. This can also be used to +remove the default `cmd` and PowerShell profiles, if the user does not wish to +see them. + ## Color Schemes Each scheme defines the color values to be used for various terminal escape sequences. @@ -97,6 +138,62 @@ Each schema is identified by the name field. Examples include The schema name can then be referenced in one or more profiles. +## Settings layering + +The runtime settings are actually constructed from _three_ sources: +* The default settings, which are hardcoded into the application, and available + in `defaults.json`. This includes the default keybindings, color schemes, and + profiles for both Windows PowerShell and Command Prompt (`cmd.exe`). +* Dynamic Profiles, which are generated at runtime. These include Powershell + Core, the Azure Cloud Shell connector, and profiles for and WSL distros. +* The user settings from `profiles.json`. + +Settings from each of these sources are "layered" upon the settings from +previous sources. In this manner, the user settings in `profiles.json` can +contain _only the changes from the default settings_. For example, if a user +would like to only change the color scheme of the default `cmd` profile to +"Solarized Dark", you could change your cmd profile to the following: + +```js + { + // Make changes here to the cmd.exe profile + "guid": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}", + "colorScheme": "Solarized Dark" + } +``` + +Here, we're know we're changing the `cmd` profile, because the `guid` +`"{0caa0dad-35be-5f56-a8ff-afceeeaa6101}"` is `cmd`'s unique GUID. Any profiles +with that GUID will all be treated as the same object. Any changes in that +profile will overwrite those from the defaults. + +Similarly, you can overwrite settings from a color scheme by defining a color +scheme in `profiles.json` with the same name as a default color scheme. + +If you'd like to unbind a keystroke that's bound to an action in the default +keybindings, you can set the `"command"` to `"unbound"` or `null`. This will +allow the keystroke to fallthough to the commandline application instead of +performing the default action. + +### Dynamic Profiles + +When dynamic profiles are created at runtime, they'll be added to the +`profiles.json` file. You can identify these profiles by the presence of a +`"source"` property. These profiles are tied to their source - if you uninstall +a linux distro, then the profile will remain in your `profiles.json` file, but +the profile will be hidden. + +If you'd like to disable a particular dynamic profile source, you can add that +`source` to the global `"disabledProfileSources"` array. For example, if you'd +like to hide all the WSL profiles, you could add the following setting: + +```json + + "disabledProfileSources": ["Microsoft.Terminal.WSL"], + ... + +``` + ## Configuration Examples: ### Add a custom background to the WSL Debian terminal profile @@ -127,15 +224,17 @@ then you should use the URI style path name given in the above example. More information about UWP URI schemes [here](https://docs.microsoft.com/en-us/windows/uwp/app-resources/uri-schemes). 3. Instead of using a UWP URI you can use a: 1. URL such as -`http://open.esa.int/files/2017/03/Mayer_and_Bond_craters_seen_by_SMART-1-350x346.jpg` +`http://open.esa.int/files/2017/03/Mayer_and_Bond_craters_seen_by_SMART-1-350x346.jpg` 2. Local file location such as `C:\Users\Public\Pictures\openlogo.jpg` -### Adding Copy and Paste Keybindings +### Adding Copy and Paste Keybindings -As of [#1093](https://github.com/microsoft/terminal/pull/1093) (first available in Windows Terminal v0.3), the Windows Terminal now -supports copy and paste keyboard shortcuts. However, if you installed and ran -the terminal before that, you won't automatically get the new keybindings added -to your settings. If you'd like to add shortcuts for copy and paste, you can do so by inserting the following objects into your `globals.keybindings` array: +As of [#1093](https://github.com/microsoft/terminal/pull/1093) (first available +in Windows Terminal v0.3), the Windows Terminal now supports copy and paste +keyboard shortcuts. However, if you installed and ran the terminal before that, +you won't automatically get the new keybindings added to your settings. If you'd +like to add shortcuts for copy and paste, you can do so by inserting the +following objects into your `globals.keybindings` array: ```json { "command": "copy", "keys": ["ctrl+shift+c"] }, @@ -171,5 +270,8 @@ You can even set multiple keybindings for a single action if you'd like. For exa will bind both ctrl+shift+v and shift+Insert to `paste`. -Note: If you set your copy keybinding to `"ctrl+c"`, you'll only be able to send an interrupt to the commandline application using Ctrl+C when there's no text selection. -Additionally, if you set `paste` to `"ctrl+v"`, commandline applications won't be able to read a ctrl+v from the input. For these reasons, we suggest `"ctrl+shift+c"` and `"ctrl+shift+v"` +Note: If you set your copy keybinding to `"ctrl+c"`, you'll only be able to send +an interrupt to the commandline application using Ctrl+C when there's +no text selection. Additionally, if you set `paste` to `"ctrl+v"`, commandline +applications won't be able to read a ctrl+v from the input. For these reasons, +we suggest `"ctrl+shift+c"` and `"ctrl+shift+v"` diff --git a/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj b/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj index 6c3839f370f..11922aa043b 100644 --- a/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj +++ b/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj @@ -249,6 +249,7 @@ Images\Wide310x150Logo.scale-400.png + @@ -259,9 +260,16 @@ + + + defaults.json + + + + diff --git a/src/cascadia/LocalTests_TerminalApp/ColorSchemeTests.cpp b/src/cascadia/LocalTests_TerminalApp/ColorSchemeTests.cpp new file mode 100644 index 00000000000..08e27bcfc3a --- /dev/null +++ b/src/cascadia/LocalTests_TerminalApp/ColorSchemeTests.cpp @@ -0,0 +1,234 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "../TerminalApp/ColorScheme.h" +#include "../TerminalApp/CascadiaSettings.h" +#include "JsonTestClass.h" + +using namespace Microsoft::Console; +using namespace TerminalApp; +using namespace WEX::Logging; +using namespace WEX::TestExecution; +using namespace WEX::Common; + +namespace TerminalAppLocalTests +{ + // Unfortunately, these tests _WILL NOT_ work in our CI, until we have a lab + // machine available that can run Windows version 18362. + + class ColorSchemeTests : public JsonTestClass + { + // Use a custom manifest to ensure that we can activate winrt types from + // our test. This property will tell taef to manually use this as the + // sxs manifest during this test class. It includes all the cppwinrt + // types we've defined, so if your test is crashing for an unknown + // reason, make sure it's included in that file. + // If you want to do anything XAML-y, you'll need to run your test in a + // packaged context. See TabTests.cpp for more details on that. + BEGIN_TEST_CLASS(ColorSchemeTests) + TEST_CLASS_PROPERTY(L"ActivationContext", L"TerminalApp.LocalTests.manifest") + END_TEST_CLASS() + + TEST_METHOD(CanLayerColorScheme); + TEST_METHOD(LayerColorSchemeProperties); + TEST_METHOD(LayerColorSchemesOnArray); + + TEST_CLASS_SETUP(ClassSetup) + { + InitializeJsonReader(); + return true; + } + }; + + void ColorSchemeTests::CanLayerColorScheme() + { + const std::string scheme0String{ R"({ + "name": "scheme0", + "foreground": "#000000", + "background": "#010101" + })" }; + const std::string scheme1String{ R"({ + "name": "scheme1", + "foreground": "#020202", + "background": "#030303" + })" }; + const std::string scheme2String{ R"({ + "name": "scheme0", + "foreground": "#040404", + "background": "#050505" + })" }; + const std::string scheme3String{ R"({ + // "name": "scheme3", + "foreground": "#060606", + "background": "#070707" + })" }; + + const auto scheme0Json = VerifyParseSucceeded(scheme0String); + const auto scheme1Json = VerifyParseSucceeded(scheme1String); + const auto scheme2Json = VerifyParseSucceeded(scheme2String); + const auto scheme3Json = VerifyParseSucceeded(scheme3String); + + const auto scheme0 = ColorScheme::FromJson(scheme0Json); + + VERIFY_IS_TRUE(scheme0.ShouldBeLayered(scheme0Json)); + VERIFY_IS_FALSE(scheme0.ShouldBeLayered(scheme1Json)); + VERIFY_IS_TRUE(scheme0.ShouldBeLayered(scheme2Json)); + VERIFY_IS_FALSE(scheme0.ShouldBeLayered(scheme3Json)); + + const auto scheme1 = ColorScheme::FromJson(scheme1Json); + + VERIFY_IS_FALSE(scheme1.ShouldBeLayered(scheme0Json)); + VERIFY_IS_TRUE(scheme1.ShouldBeLayered(scheme1Json)); + VERIFY_IS_FALSE(scheme1.ShouldBeLayered(scheme2Json)); + VERIFY_IS_FALSE(scheme1.ShouldBeLayered(scheme3Json)); + + const auto scheme3 = ColorScheme::FromJson(scheme3Json); + + VERIFY_IS_FALSE(scheme3.ShouldBeLayered(scheme0Json)); + VERIFY_IS_FALSE(scheme3.ShouldBeLayered(scheme1Json)); + VERIFY_IS_FALSE(scheme3.ShouldBeLayered(scheme2Json)); + VERIFY_IS_FALSE(scheme3.ShouldBeLayered(scheme3Json)); + } + + void ColorSchemeTests::LayerColorSchemeProperties() + { + const std::string scheme0String{ R"({ + "name": "scheme0", + "foreground": "#000000", + "background": "#010101", + "red": "#010000", + "green": "#000100", + "blue": "#000001" + })" }; + const std::string scheme1String{ R"({ + "name": "scheme1", + "foreground": "#020202", + "background": "#030303", + "red": "#020000", + + "blue": "#000002" + })" }; + const std::string scheme2String{ R"({ + "name": "scheme0", + "foreground": "#040404", + "background": "#050505", + "red": "#030000", + "green": "#000300" + })" }; + + const auto scheme0Json = VerifyParseSucceeded(scheme0String); + const auto scheme1Json = VerifyParseSucceeded(scheme1String); + const auto scheme2Json = VerifyParseSucceeded(scheme2String); + + auto scheme0 = ColorScheme::FromJson(scheme0Json); + VERIFY_ARE_EQUAL(L"scheme0", scheme0._schemeName); + VERIFY_ARE_EQUAL(ARGB(0, 0, 0, 0), scheme0._defaultForeground); + VERIFY_ARE_EQUAL(ARGB(0, 1, 1, 1), scheme0._defaultBackground); + VERIFY_ARE_EQUAL(ARGB(0, 1, 0, 0), scheme0._table[XTERM_RED_ATTR]); + VERIFY_ARE_EQUAL(ARGB(0, 0, 1, 0), scheme0._table[XTERM_GREEN_ATTR]); + VERIFY_ARE_EQUAL(ARGB(0, 0, 0, 1), scheme0._table[XTERM_BLUE_ATTR]); + + Log::Comment(NoThrowString().Format( + L"Layering scheme1 on top of scheme0")); + scheme0.LayerJson(scheme1Json); + + VERIFY_ARE_EQUAL(ARGB(0, 2, 2, 2), scheme0._defaultForeground); + VERIFY_ARE_EQUAL(ARGB(0, 3, 3, 3), scheme0._defaultBackground); + VERIFY_ARE_EQUAL(ARGB(0, 2, 0, 0), scheme0._table[XTERM_RED_ATTR]); + VERIFY_ARE_EQUAL(ARGB(0, 0, 1, 0), scheme0._table[XTERM_GREEN_ATTR]); + VERIFY_ARE_EQUAL(ARGB(0, 0, 0, 2), scheme0._table[XTERM_BLUE_ATTR]); + + Log::Comment(NoThrowString().Format( + L"Layering scheme2Json on top of (scheme0+scheme1)")); + scheme0.LayerJson(scheme2Json); + + VERIFY_ARE_EQUAL(ARGB(0, 4, 4, 4), scheme0._defaultForeground); + VERIFY_ARE_EQUAL(ARGB(0, 5, 5, 5), scheme0._defaultBackground); + VERIFY_ARE_EQUAL(ARGB(0, 3, 0, 0), scheme0._table[XTERM_RED_ATTR]); + VERIFY_ARE_EQUAL(ARGB(0, 0, 3, 0), scheme0._table[XTERM_GREEN_ATTR]); + VERIFY_ARE_EQUAL(ARGB(0, 0, 0, 2), scheme0._table[XTERM_BLUE_ATTR]); + } + + void ColorSchemeTests::LayerColorSchemesOnArray() + { + const std::string scheme0String{ R"({ + "name": "scheme0", + "foreground": "#000000", + "background": "#010101" + })" }; + const std::string scheme1String{ R"({ + "name": "scheme1", + "foreground": "#020202", + "background": "#030303" + })" }; + const std::string scheme2String{ R"({ + "name": "scheme0", + "foreground": "#040404", + "background": "#050505" + })" }; + const std::string scheme3String{ R"({ + // "name": "scheme3", + "foreground": "#060606", + "background": "#070707" + })" }; + + const auto scheme0Json = VerifyParseSucceeded(scheme0String); + const auto scheme1Json = VerifyParseSucceeded(scheme1String); + const auto scheme2Json = VerifyParseSucceeded(scheme2String); + const auto scheme3Json = VerifyParseSucceeded(scheme3String); + + CascadiaSettings settings; + + VERIFY_ARE_EQUAL(0u, settings._globals.GetColorSchemes().size()); + VERIFY_IS_NULL(settings._FindMatchingColorScheme(scheme0Json)); + VERIFY_IS_NULL(settings._FindMatchingColorScheme(scheme1Json)); + VERIFY_IS_NULL(settings._FindMatchingColorScheme(scheme2Json)); + VERIFY_IS_NULL(settings._FindMatchingColorScheme(scheme3Json)); + + settings._LayerOrCreateColorScheme(scheme0Json); + VERIFY_ARE_EQUAL(1u, settings._globals.GetColorSchemes().size()); + VERIFY_IS_NOT_NULL(settings._FindMatchingColorScheme(scheme0Json)); + VERIFY_IS_NULL(settings._FindMatchingColorScheme(scheme1Json)); + VERIFY_IS_NOT_NULL(settings._FindMatchingColorScheme(scheme2Json)); + VERIFY_IS_NULL(settings._FindMatchingColorScheme(scheme3Json)); + VERIFY_ARE_EQUAL(ARGB(0, 0, 0, 0), settings._globals.GetColorSchemes().at(0)._defaultForeground); + VERIFY_ARE_EQUAL(ARGB(0, 1, 1, 1), settings._globals.GetColorSchemes().at(0)._defaultBackground); + + settings._LayerOrCreateColorScheme(scheme1Json); + VERIFY_ARE_EQUAL(2u, settings._globals.GetColorSchemes().size()); + VERIFY_IS_NOT_NULL(settings._FindMatchingColorScheme(scheme0Json)); + VERIFY_IS_NOT_NULL(settings._FindMatchingColorScheme(scheme1Json)); + VERIFY_IS_NOT_NULL(settings._FindMatchingColorScheme(scheme2Json)); + VERIFY_IS_NULL(settings._FindMatchingColorScheme(scheme3Json)); + VERIFY_ARE_EQUAL(ARGB(0, 0, 0, 0), settings._globals.GetColorSchemes().at(0)._defaultForeground); + VERIFY_ARE_EQUAL(ARGB(0, 1, 1, 1), settings._globals.GetColorSchemes().at(0)._defaultBackground); + VERIFY_ARE_EQUAL(ARGB(0, 2, 2, 2), settings._globals.GetColorSchemes().at(1)._defaultForeground); + VERIFY_ARE_EQUAL(ARGB(0, 3, 3, 3), settings._globals.GetColorSchemes().at(1)._defaultBackground); + + settings._LayerOrCreateColorScheme(scheme2Json); + VERIFY_ARE_EQUAL(2u, settings._globals.GetColorSchemes().size()); + VERIFY_IS_NOT_NULL(settings._FindMatchingColorScheme(scheme0Json)); + VERIFY_IS_NOT_NULL(settings._FindMatchingColorScheme(scheme1Json)); + VERIFY_IS_NOT_NULL(settings._FindMatchingColorScheme(scheme2Json)); + VERIFY_IS_NULL(settings._FindMatchingColorScheme(scheme3Json)); + VERIFY_ARE_EQUAL(ARGB(0, 4, 4, 4), settings._globals.GetColorSchemes().at(0)._defaultForeground); + VERIFY_ARE_EQUAL(ARGB(0, 5, 5, 5), settings._globals.GetColorSchemes().at(0)._defaultBackground); + VERIFY_ARE_EQUAL(ARGB(0, 2, 2, 2), settings._globals.GetColorSchemes().at(1)._defaultForeground); + VERIFY_ARE_EQUAL(ARGB(0, 3, 3, 3), settings._globals.GetColorSchemes().at(1)._defaultBackground); + + settings._LayerOrCreateColorScheme(scheme3Json); + VERIFY_ARE_EQUAL(3u, settings._globals.GetColorSchemes().size()); + VERIFY_IS_NOT_NULL(settings._FindMatchingColorScheme(scheme0Json)); + VERIFY_IS_NOT_NULL(settings._FindMatchingColorScheme(scheme1Json)); + VERIFY_IS_NOT_NULL(settings._FindMatchingColorScheme(scheme2Json)); + VERIFY_IS_NULL(settings._FindMatchingColorScheme(scheme3Json)); + VERIFY_ARE_EQUAL(ARGB(0, 4, 4, 4), settings._globals.GetColorSchemes().at(0)._defaultForeground); + VERIFY_ARE_EQUAL(ARGB(0, 5, 5, 5), settings._globals.GetColorSchemes().at(0)._defaultBackground); + VERIFY_ARE_EQUAL(ARGB(0, 2, 2, 2), settings._globals.GetColorSchemes().at(1)._defaultForeground); + VERIFY_ARE_EQUAL(ARGB(0, 3, 3, 3), settings._globals.GetColorSchemes().at(1)._defaultBackground); + VERIFY_ARE_EQUAL(ARGB(0, 6, 6, 6), settings._globals.GetColorSchemes().at(2)._defaultForeground); + VERIFY_ARE_EQUAL(ARGB(0, 7, 7, 7), settings._globals.GetColorSchemes().at(2)._defaultBackground); + } +} diff --git a/src/cascadia/LocalTests_TerminalApp/JsonTestClass.h b/src/cascadia/LocalTests_TerminalApp/JsonTestClass.h new file mode 100644 index 00000000000..a66b9f2af58 --- /dev/null +++ b/src/cascadia/LocalTests_TerminalApp/JsonTestClass.h @@ -0,0 +1,36 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- JsonTestClass.h + +Abstract: +- This class is a helper that can be used to quickly create tests that need to + read & parse json data. Test classes that need to read JSON should make sure + to derive from this class, and also make sure to call InitializeJsonReader() + in the TEST_CLASS_SETUP(). + +Author(s): + Mike Griese (migrie) August-2019 +--*/ + +class JsonTestClass +{ +public: + void InitializeJsonReader() + { + _reader = std::unique_ptr(Json::CharReaderBuilder::CharReaderBuilder().newCharReader()); + }; + Json::Value VerifyParseSucceeded(std::string content) + { + Json::Value root; + std::string errs; + const bool parseResult = _reader->parse(content.c_str(), content.c_str() + content.size(), &root, &errs); + VERIFY_IS_TRUE(parseResult, winrt::to_hstring(errs).c_str()); + return root; + }; + +protected: + std::unique_ptr _reader; +}; diff --git a/src/cascadia/LocalTests_TerminalApp/KeyBindingsTests.cpp b/src/cascadia/LocalTests_TerminalApp/KeyBindingsTests.cpp new file mode 100644 index 00000000000..cddd26466fe --- /dev/null +++ b/src/cascadia/LocalTests_TerminalApp/KeyBindingsTests.cpp @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "../TerminalApp/ColorScheme.h" +#include "../TerminalApp/CascadiaSettings.h" +#include "JsonTestClass.h" + +using namespace Microsoft::Console; +using namespace TerminalApp; +using namespace WEX::Logging; +using namespace WEX::TestExecution; +using namespace WEX::Common; + +namespace TerminalAppLocalTests +{ + // Unfortunately, these tests _WILL NOT_ work in our CI, until we have a lab + // machine available that can run Windows version 18362. + + class KeyBindingsTests : public JsonTestClass + { + // Use a custom manifest to ensure that we can activate winrt types from + // our test. This property will tell taef to manually use this as the + // sxs manifest during this test class. It includes all the cppwinrt + // types we've defined, so if your test is crashing for an unknown + // reason, make sure it's included in that file. + // If you want to do anything XAML-y, you'll need to run your test in a + // packaged context. See TabTests.cpp for more details on that. + BEGIN_TEST_CLASS(KeyBindingsTests) + TEST_CLASS_PROPERTY(L"ActivationContext", L"TerminalApp.LocalTests.manifest") + END_TEST_CLASS() + + TEST_METHOD(ManyKeysSameAction); + TEST_METHOD(LayerKeybindings); + TEST_METHOD(UnbindKeybindings); + + TEST_CLASS_SETUP(ClassSetup) + { + InitializeJsonReader(); + return true; + } + }; + + void KeyBindingsTests::ManyKeysSameAction() + { + const std::string bindings0String{ R"([ { "command": "copy", "keys": ["ctrl+c"] } ])" }; + const std::string bindings1String{ R"([ { "command": "copy", "keys": ["enter"] } ])" }; + const std::string bindings2String{ R"([ + { "command": "paste", "keys": ["ctrl+v"] }, + { "command": "paste", "keys": ["ctrl+shift+v"] } + ])" }; + + const auto bindings0Json = VerifyParseSucceeded(bindings0String); + const auto bindings1Json = VerifyParseSucceeded(bindings1String); + const auto bindings2Json = VerifyParseSucceeded(bindings2String); + + auto appKeyBindings = winrt::make_self(); + VERIFY_IS_NOT_NULL(appKeyBindings); + VERIFY_ARE_EQUAL(0u, appKeyBindings->_keyShortcuts.size()); + + appKeyBindings->LayerJson(bindings0Json); + VERIFY_ARE_EQUAL(1u, appKeyBindings->_keyShortcuts.size()); + + appKeyBindings->LayerJson(bindings1Json); + VERIFY_ARE_EQUAL(2u, appKeyBindings->_keyShortcuts.size()); + + appKeyBindings->LayerJson(bindings2Json); + VERIFY_ARE_EQUAL(4u, appKeyBindings->_keyShortcuts.size()); + } + + void KeyBindingsTests::LayerKeybindings() + { + const std::string bindings0String{ R"([ { "command": "copy", "keys": ["ctrl+c"] } ])" }; + const std::string bindings1String{ R"([ { "command": "paste", "keys": ["ctrl+c"] } ])" }; + const std::string bindings2String{ R"([ { "command": "copy", "keys": ["enter"] } ])" }; + + const auto bindings0Json = VerifyParseSucceeded(bindings0String); + const auto bindings1Json = VerifyParseSucceeded(bindings1String); + const auto bindings2Json = VerifyParseSucceeded(bindings2String); + + auto appKeyBindings = winrt::make_self(); + VERIFY_IS_NOT_NULL(appKeyBindings); + VERIFY_ARE_EQUAL(0u, appKeyBindings->_keyShortcuts.size()); + + appKeyBindings->LayerJson(bindings0Json); + VERIFY_ARE_EQUAL(1u, appKeyBindings->_keyShortcuts.size()); + + appKeyBindings->LayerJson(bindings1Json); + VERIFY_ARE_EQUAL(1u, appKeyBindings->_keyShortcuts.size()); + + appKeyBindings->LayerJson(bindings2Json); + VERIFY_ARE_EQUAL(2u, appKeyBindings->_keyShortcuts.size()); + } + + void KeyBindingsTests::UnbindKeybindings() + { + const std::string bindings0String{ R"([ { "command": "copy", "keys": ["ctrl+c"] } ])" }; + const std::string bindings1String{ R"([ { "command": "paste", "keys": ["ctrl+c"] } ])" }; + const std::string bindings2String{ R"([ { "command": "unbound", "keys": ["ctrl+c"] } ])" }; + const std::string bindings3String{ R"([ { "command": null, "keys": ["ctrl+c"] } ])" }; + const std::string bindings4String{ R"([ { "command": "garbage", "keys": ["ctrl+c"] } ])" }; + const std::string bindings5String{ R"([ { "command": 5, "keys": ["ctrl+c"] } ])" }; + + const auto bindings0Json = VerifyParseSucceeded(bindings0String); + const auto bindings1Json = VerifyParseSucceeded(bindings1String); + const auto bindings2Json = VerifyParseSucceeded(bindings2String); + const auto bindings3Json = VerifyParseSucceeded(bindings3String); + const auto bindings4Json = VerifyParseSucceeded(bindings4String); + const auto bindings5Json = VerifyParseSucceeded(bindings5String); + + auto appKeyBindings = winrt::make_self(); + VERIFY_IS_NOT_NULL(appKeyBindings); + VERIFY_ARE_EQUAL(0u, appKeyBindings->_keyShortcuts.size()); + + appKeyBindings->LayerJson(bindings0Json); + VERIFY_ARE_EQUAL(1u, appKeyBindings->_keyShortcuts.size()); + + appKeyBindings->LayerJson(bindings1Json); + VERIFY_ARE_EQUAL(1u, appKeyBindings->_keyShortcuts.size()); + + Log::Comment(NoThrowString().Format( + L"Try unbinding a key using `\"unbound\"` to unbind the key")); + appKeyBindings->LayerJson(bindings2Json); + VERIFY_ARE_EQUAL(0u, appKeyBindings->_keyShortcuts.size()); + + Log::Comment(NoThrowString().Format( + L"Try unbinding a key using `null` to unbind the key")); + // First add back a good binding + appKeyBindings->LayerJson(bindings0Json); + VERIFY_ARE_EQUAL(1u, appKeyBindings->_keyShortcuts.size()); + // Then try layering in the bad setting + appKeyBindings->LayerJson(bindings3Json); + VERIFY_ARE_EQUAL(0u, appKeyBindings->_keyShortcuts.size()); + + Log::Comment(NoThrowString().Format( + L"Try unbinding a key using an unrecognized command to unbind the key")); + // First add back a good binding + appKeyBindings->LayerJson(bindings0Json); + VERIFY_ARE_EQUAL(1u, appKeyBindings->_keyShortcuts.size()); + // Then try layering in the bad setting + appKeyBindings->LayerJson(bindings4Json); + VERIFY_ARE_EQUAL(0u, appKeyBindings->_keyShortcuts.size()); + + Log::Comment(NoThrowString().Format( + L"Try unbinding a key using a straight up invalid value to unbind the key")); + // First add back a good binding + appKeyBindings->LayerJson(bindings0Json); + VERIFY_ARE_EQUAL(1u, appKeyBindings->_keyShortcuts.size()); + // Then try layering in the bad setting + appKeyBindings->LayerJson(bindings5Json); + VERIFY_ARE_EQUAL(0u, appKeyBindings->_keyShortcuts.size()); + + Log::Comment(NoThrowString().Format( + L"Try unbinding a key that wasn't bound at all")); + appKeyBindings->LayerJson(bindings2Json); + VERIFY_ARE_EQUAL(0u, appKeyBindings->_keyShortcuts.size()); + } +} diff --git a/src/cascadia/LocalTests_TerminalApp/ProfileTests.cpp b/src/cascadia/LocalTests_TerminalApp/ProfileTests.cpp new file mode 100644 index 00000000000..de73b2da56c --- /dev/null +++ b/src/cascadia/LocalTests_TerminalApp/ProfileTests.cpp @@ -0,0 +1,296 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "../TerminalApp/ColorScheme.h" +#include "../TerminalApp/CascadiaSettings.h" +#include "JsonTestClass.h" + +using namespace Microsoft::Console; +using namespace TerminalApp; +using namespace WEX::Logging; +using namespace WEX::TestExecution; +using namespace WEX::Common; + +namespace TerminalAppLocalTests +{ + // Unfortunately, these tests _WILL NOT_ work in our CI, until we have a lab + // machine available that can run Windows version 18362. + + class ProfileTests : public JsonTestClass + { + // Use a custom manifest to ensure that we can activate winrt types from + // our test. This property will tell taef to manually use this as the + // sxs manifest during this test class. It includes all the cppwinrt + // types we've defined, so if your test is crashing for an unknown + // reason, make sure it's included in that file. + // If you want to do anything XAML-y, you'll need to run your test in a + // packaged context. See TabTests.cpp for more details on that. + BEGIN_TEST_CLASS(ProfileTests) + TEST_CLASS_PROPERTY(L"ActivationContext", L"TerminalApp.LocalTests.manifest") + END_TEST_CLASS() + + TEST_METHOD(CanLayerProfile); + TEST_METHOD(LayerProfileProperties); + TEST_METHOD(LayerProfileIcon); + TEST_METHOD(LayerProfilesOnArray); + + TEST_CLASS_SETUP(ClassSetup) + { + InitializeJsonReader(); + return true; + } + }; + + void ProfileTests::CanLayerProfile() + { + const std::string profile0String{ R"({ + "name" : "profile0", + "guid" : "{6239a42c-1111-49a3-80bd-e8fdd045185c}" + })" }; + const std::string profile1String{ R"({ + "name" : "profile1", + "guid" : "{6239a42c-2222-49a3-80bd-e8fdd045185c}" + })" }; + const std::string profile2String{ R"({ + "name" : "profile2", + "guid" : "{6239a42c-1111-49a3-80bd-e8fdd045185c}" + })" }; + const std::string profile3String{ R"({ + "name" : "profile3" + })" }; + + const auto profile0Json = VerifyParseSucceeded(profile0String); + const auto profile1Json = VerifyParseSucceeded(profile1String); + const auto profile2Json = VerifyParseSucceeded(profile2String); + const auto profile3Json = VerifyParseSucceeded(profile3String); + + const auto profile0 = Profile::FromJson(profile0Json); + + VERIFY_IS_FALSE(profile0.ShouldBeLayered(profile1Json)); + VERIFY_IS_TRUE(profile0.ShouldBeLayered(profile2Json)); + VERIFY_IS_FALSE(profile0.ShouldBeLayered(profile3Json)); + + const auto profile1 = Profile::FromJson(profile1Json); + + VERIFY_IS_FALSE(profile1.ShouldBeLayered(profile0Json)); + // A profile _can_ be layered with itself, though what's the point? + VERIFY_IS_TRUE(profile1.ShouldBeLayered(profile1Json)); + VERIFY_IS_FALSE(profile1.ShouldBeLayered(profile2Json)); + VERIFY_IS_FALSE(profile1.ShouldBeLayered(profile3Json)); + + const auto profile3 = Profile::FromJson(profile3Json); + + VERIFY_IS_FALSE(profile3.ShouldBeLayered(profile0Json)); + // A profile _can_ be layered with itself, though what's the point? + VERIFY_IS_FALSE(profile3.ShouldBeLayered(profile1Json)); + VERIFY_IS_FALSE(profile3.ShouldBeLayered(profile2Json)); + VERIFY_IS_FALSE(profile3.ShouldBeLayered(profile3Json)); + } + + void ProfileTests::LayerProfileProperties() + { + const std::string profile0String{ R"({ + "name": "profile0", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "foreground": "#000000", + "background": "#010101" + })" }; + const std::string profile1String{ R"({ + "name": "profile1", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "foreground": "#020202", + "startingDirectory": "C:/" + })" }; + const std::string profile2String{ R"({ + "name": "profile2", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "foreground": "#030303" + })" }; + + const auto profile0Json = VerifyParseSucceeded(profile0String); + const auto profile1Json = VerifyParseSucceeded(profile1String); + const auto profile2Json = VerifyParseSucceeded(profile2String); + + auto profile0 = Profile::FromJson(profile0Json); + VERIFY_IS_TRUE(profile0._defaultForeground.has_value()); + VERIFY_ARE_EQUAL(ARGB(0, 0, 0, 0), profile0._defaultForeground.value()); + + VERIFY_IS_TRUE(profile0._defaultBackground.has_value()); + VERIFY_ARE_EQUAL(ARGB(0, 1, 1, 1), profile0._defaultBackground.value()); + + VERIFY_ARE_EQUAL(L"profile0", profile0._name); + + VERIFY_IS_FALSE(profile0._startingDirectory.has_value()); + + Log::Comment(NoThrowString().Format( + L"Layering profile1 on top of profile0")); + profile0.LayerJson(profile1Json); + + VERIFY_IS_TRUE(profile0._defaultForeground.has_value()); + VERIFY_ARE_EQUAL(ARGB(0, 2, 2, 2), profile0._defaultForeground.value()); + + VERIFY_IS_TRUE(profile0._defaultBackground.has_value()); + VERIFY_ARE_EQUAL(ARGB(0, 1, 1, 1), profile0._defaultBackground.value()); + + VERIFY_ARE_EQUAL(L"profile1", profile0._name); + + VERIFY_IS_TRUE(profile0._startingDirectory.has_value()); + VERIFY_ARE_EQUAL(L"C:/", profile0._startingDirectory.value()); + + Log::Comment(NoThrowString().Format( + L"Layering profile2 on top of (profile0+profile1)")); + profile0.LayerJson(profile2Json); + + VERIFY_IS_TRUE(profile0._defaultForeground.has_value()); + VERIFY_ARE_EQUAL(ARGB(0, 3, 3, 3), profile0._defaultForeground.value()); + + VERIFY_IS_TRUE(profile0._defaultBackground.has_value()); + VERIFY_ARE_EQUAL(ARGB(0, 1, 1, 1), profile0._defaultBackground.value()); + + VERIFY_ARE_EQUAL(L"profile2", profile0._name); + + VERIFY_IS_TRUE(profile0._startingDirectory.has_value()); + VERIFY_ARE_EQUAL(L"C:/", profile0._startingDirectory.value()); + } + + void ProfileTests::LayerProfileIcon() + { + const std::string profile0String{ R"({ + "name": "profile0", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "icon": "not-null.png" + })" }; + const std::string profile1String{ R"({ + "name": "profile1", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "icon": null + })" }; + const std::string profile2String{ R"({ + "name": "profile2", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}" + })" }; + const std::string profile3String{ R"({ + "name": "profile3", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "icon": "another-real.png" + })" }; + + const auto profile0Json = VerifyParseSucceeded(profile0String); + const auto profile1Json = VerifyParseSucceeded(profile1String); + const auto profile2Json = VerifyParseSucceeded(profile2String); + const auto profile3Json = VerifyParseSucceeded(profile3String); + + auto profile0 = Profile::FromJson(profile0Json); + VERIFY_IS_TRUE(profile0._icon.has_value()); + VERIFY_ARE_EQUAL(L"not-null.png", profile0._icon.value()); + + Log::Comment(NoThrowString().Format( + L"Verify that layering an object the key set to null will clear the key")); + profile0.LayerJson(profile1Json); + VERIFY_IS_FALSE(profile0._icon.has_value()); + + profile0.LayerJson(profile2Json); + VERIFY_IS_FALSE(profile0._icon.has_value()); + + profile0.LayerJson(profile3Json); + VERIFY_IS_TRUE(profile0._icon.has_value()); + VERIFY_ARE_EQUAL(L"another-real.png", profile0._icon.value()); + + Log::Comment(NoThrowString().Format( + L"Verify that layering an object _without_ the key will not clear the key")); + profile0.LayerJson(profile2Json); + VERIFY_IS_TRUE(profile0._icon.has_value()); + VERIFY_ARE_EQUAL(L"another-real.png", profile0._icon.value()); + + auto profile1 = Profile::FromJson(profile1Json); + VERIFY_IS_FALSE(profile1._icon.has_value()); + profile1.LayerJson(profile3Json); + VERIFY_IS_TRUE(profile1._icon.has_value()); + VERIFY_ARE_EQUAL(L"another-real.png", profile1._icon.value()); + } + + void ProfileTests::LayerProfilesOnArray() + { + const std::string profile0String{ R"({ + "name" : "profile0", + "guid" : "{6239a42c-0000-49a3-80bd-e8fdd045185c}" + })" }; + const std::string profile1String{ R"({ + "name" : "profile1", + "guid" : "{6239a42c-1111-49a3-80bd-e8fdd045185c}" + })" }; + const std::string profile2String{ R"({ + "name" : "profile2", + "guid" : "{6239a42c-2222-49a3-80bd-e8fdd045185c}" + })" }; + const std::string profile3String{ R"({ + "name" : "profile3", + "guid" : "{6239a42c-0000-49a3-80bd-e8fdd045185c}" + })" }; + const std::string profile4String{ R"({ + "name" : "profile4", + "guid" : "{6239a42c-0000-49a3-80bd-e8fdd045185c}" + })" }; + + const auto profile0Json = VerifyParseSucceeded(profile0String); + const auto profile1Json = VerifyParseSucceeded(profile1String); + const auto profile2Json = VerifyParseSucceeded(profile2String); + const auto profile3Json = VerifyParseSucceeded(profile3String); + const auto profile4Json = VerifyParseSucceeded(profile4String); + + CascadiaSettings settings; + + VERIFY_ARE_EQUAL(0u, settings._profiles.size()); + VERIFY_IS_NULL(settings._FindMatchingProfile(profile0Json)); + VERIFY_IS_NULL(settings._FindMatchingProfile(profile1Json)); + VERIFY_IS_NULL(settings._FindMatchingProfile(profile2Json)); + VERIFY_IS_NULL(settings._FindMatchingProfile(profile3Json)); + VERIFY_IS_NULL(settings._FindMatchingProfile(profile4Json)); + + settings._LayerOrCreateProfile(profile0Json); + VERIFY_ARE_EQUAL(1u, settings._profiles.size()); + VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile0Json)); + VERIFY_IS_NULL(settings._FindMatchingProfile(profile1Json)); + VERIFY_IS_NULL(settings._FindMatchingProfile(profile2Json)); + VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile3Json)); + VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile4Json)); + + settings._LayerOrCreateProfile(profile1Json); + VERIFY_ARE_EQUAL(2u, settings._profiles.size()); + VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile0Json)); + VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile1Json)); + VERIFY_IS_NULL(settings._FindMatchingProfile(profile2Json)); + VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile3Json)); + VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile4Json)); + + settings._LayerOrCreateProfile(profile2Json); + VERIFY_ARE_EQUAL(3u, settings._profiles.size()); + VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile0Json)); + VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile1Json)); + VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile2Json)); + VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile3Json)); + VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile4Json)); + VERIFY_ARE_EQUAL(L"profile0", settings._profiles.at(0)._name); + + settings._LayerOrCreateProfile(profile3Json); + VERIFY_ARE_EQUAL(3u, settings._profiles.size()); + VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile0Json)); + VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile1Json)); + VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile2Json)); + VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile3Json)); + VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile4Json)); + VERIFY_ARE_EQUAL(L"profile3", settings._profiles.at(0)._name); + + settings._LayerOrCreateProfile(profile4Json); + VERIFY_ARE_EQUAL(3u, settings._profiles.size()); + VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile0Json)); + VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile1Json)); + VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile2Json)); + VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile3Json)); + VERIFY_IS_NOT_NULL(settings._FindMatchingProfile(profile4Json)); + VERIFY_ARE_EQUAL(L"profile4", settings._profiles.at(0)._name); + } + +} diff --git a/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp b/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp index 4a70386e33d..6c1e5950db8 100644 --- a/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp +++ b/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp @@ -5,6 +5,8 @@ #include "../TerminalApp/ColorScheme.h" #include "../TerminalApp/CascadiaSettings.h" +#include "JsonTestClass.h" +#include using namespace Microsoft::Console; using namespace TerminalApp; @@ -17,14 +19,14 @@ namespace TerminalAppLocalTests // Unfortunately, these tests _WILL NOT_ work in our CI, until we have a lab // machine available that can run Windows version 18362. - class SettingsTests + class SettingsTests : public JsonTestClass { // Use a custom manifest to ensure that we can activate winrt types from // our test. This property will tell taef to manually use this as the // sxs manifest during this test class. It includes all the cppwinrt // types we've defined, so if your test is crashing for an unknown // reason, make sure it's included in that file. - // If you want to do anything XAML-y, you'll need to run yor test in a + // If you want to do anything XAML-y, you'll need to run your test in a // packaged context. See TabTests.cpp for more details on that. BEGIN_TEST_CLASS(SettingsTests) TEST_CLASS_PROPERTY(L"ActivationContext", L"TerminalApp.LocalTests.manifest") @@ -35,30 +37,25 @@ namespace TerminalAppLocalTests TEST_METHOD(ValidateDefaultProfileExists); TEST_METHOD(ValidateDuplicateProfiles); TEST_METHOD(ValidateManyWarnings); + TEST_METHOD(LayerGlobalProperties); + TEST_METHOD(ValidateProfileOrdering); + TEST_METHOD(ValidateHideProfiles); + TEST_METHOD(ValidateProfilesGenerateGuids); + TEST_METHOD(GeneratedGuidRoundtrips); + TEST_METHOD(TestAllValidationsWithNullGuids); + TEST_METHOD(TestReorderWithNullGuids); + TEST_METHOD(TestReorderingWithoutGuid); TEST_CLASS_SETUP(ClassSetup) { - reader = std::unique_ptr(Json::CharReaderBuilder::CharReaderBuilder().newCharReader()); + InitializeJsonReader(); return true; } - Json::Value VerifyParseSucceeded(std::string content); - - private: - std::unique_ptr reader; }; - Json::Value SettingsTests::VerifyParseSucceeded(std::string content) - { - Json::Value root; - std::string errs; - const bool parseResult = reader->parse(content.c_str(), content.c_str() + content.size(), &root, &errs); - VERIFY_IS_TRUE(parseResult, winrt::to_hstring(errs).c_str()); - return root; - } - void SettingsTests::TryCreateWinRTType() { - winrt::Microsoft::Terminal::Settings::TerminalSettings settings{}; + winrt::Microsoft::Terminal::Settings::TerminalSettings settings; VERIFY_IS_NOT_NULL(settings); auto oldFontSize = settings.FontSize(); settings.FontSize(oldFontSize + 5); @@ -282,47 +279,76 @@ namespace TerminalAppLocalTests } ] })" }; + Profile profile0{ Microsoft::Console::Utils::GuidFromString(L"{6239a42c-4444-49a3-80bd-e8fdd045185c}") }; + profile0._name = L"profile0"; + Profile profile1{ Microsoft::Console::Utils::GuidFromString(L"{6239a42c-5555-49a3-80bd-e8fdd045185c}") }; + profile1._name = L"profile1"; + Profile profile2{ Microsoft::Console::Utils::GuidFromString(L"{6239a42c-4444-49a3-80bd-e8fdd045185c}") }; + profile2._name = L"profile2"; + Profile profile3{ Microsoft::Console::Utils::GuidFromString(L"{6239a42c-4444-49a3-80bd-e8fdd045185c}") }; + profile3._name = L"profile3"; + Profile profile4{ Microsoft::Console::Utils::GuidFromString(L"{6239a42c-6666-49a3-80bd-e8fdd045185c}") }; + profile4._name = L"profile4"; + Profile profile5{ Microsoft::Console::Utils::GuidFromString(L"{6239a42c-5555-49a3-80bd-e8fdd045185c}") }; + profile5._name = L"profile5"; + Profile profile6{ Microsoft::Console::Utils::GuidFromString(L"{6239a42c-7777-49a3-80bd-e8fdd045185c}") }; + profile6._name = L"profile6"; { // Case 1: Good settings Log::Comment(NoThrowString().Format( L"Testing a pair of profiles with unique guids")); - const auto settingsObject = VerifyParseSucceeded(goodProfiles); - auto settings = CascadiaSettings::FromJson(settingsObject); - settings->_ValidateNoDuplicateProfiles(); - VERIFY_ARE_EQUAL(static_cast(0), settings->_warnings.size()); - VERIFY_ARE_EQUAL(static_cast(2), settings->_profiles.size()); + + CascadiaSettings settings; + settings._profiles.push_back(profile0); + settings._profiles.push_back(profile1); + + settings._ValidateNoDuplicateProfiles(); + + VERIFY_ARE_EQUAL(static_cast(0), settings._warnings.size()); + VERIFY_ARE_EQUAL(static_cast(2), settings._profiles.size()); } { // Case 2: Bad settings Log::Comment(NoThrowString().Format( L"Testing a pair of profiles with the same guid")); - const auto settingsObject = VerifyParseSucceeded(badProfiles); - auto settings = CascadiaSettings::FromJson(settingsObject); - settings->_ValidateNoDuplicateProfiles(); + CascadiaSettings settings; + settings._profiles.push_back(profile2); + settings._profiles.push_back(profile3); - VERIFY_ARE_EQUAL(static_cast(1), settings->_warnings.size()); - VERIFY_ARE_EQUAL(::TerminalApp::SettingsLoadWarnings::DuplicateProfile, settings->_warnings.at(0)); + settings._ValidateNoDuplicateProfiles(); + + VERIFY_ARE_EQUAL(static_cast(1), settings._warnings.size()); + VERIFY_ARE_EQUAL(::TerminalApp::SettingsLoadWarnings::DuplicateProfile, settings._warnings.at(0)); - VERIFY_ARE_EQUAL(static_cast(1), settings->_profiles.size()); - VERIFY_ARE_EQUAL(L"profile0", settings->_profiles.at(0).GetName()); + VERIFY_ARE_EQUAL(static_cast(1), settings._profiles.size()); + VERIFY_ARE_EQUAL(L"profile2", settings._profiles.at(0).GetName()); } { // Case 3: Very bad settings Log::Comment(NoThrowString().Format( L"Testing a set of profiles, many of which with duplicated guids")); - const auto settingsObject = VerifyParseSucceeded(veryBadProfiles); - auto settings = CascadiaSettings::FromJson(settingsObject); - settings->_ValidateNoDuplicateProfiles(); - VERIFY_ARE_EQUAL(static_cast(1), settings->_warnings.size()); - VERIFY_ARE_EQUAL(::TerminalApp::SettingsLoadWarnings::DuplicateProfile, settings->_warnings.at(0)); - VERIFY_ARE_EQUAL(static_cast(4), settings->_profiles.size()); - VERIFY_ARE_EQUAL(L"profile0", settings->_profiles.at(0).GetName()); - VERIFY_ARE_EQUAL(L"profile1", settings->_profiles.at(1).GetName()); - VERIFY_ARE_EQUAL(L"profile4", settings->_profiles.at(2).GetName()); - VERIFY_ARE_EQUAL(L"profile6", settings->_profiles.at(3).GetName()); + CascadiaSettings settings; + settings._profiles.push_back(profile0); + settings._profiles.push_back(profile1); + settings._profiles.push_back(profile2); + settings._profiles.push_back(profile3); + settings._profiles.push_back(profile4); + settings._profiles.push_back(profile5); + settings._profiles.push_back(profile6); + + settings._ValidateNoDuplicateProfiles(); + + VERIFY_ARE_EQUAL(static_cast(1), settings._warnings.size()); + VERIFY_ARE_EQUAL(::TerminalApp::SettingsLoadWarnings::DuplicateProfile, settings._warnings.at(0)); + + VERIFY_ARE_EQUAL(static_cast(4), settings._profiles.size()); + VERIFY_ARE_EQUAL(L"profile0", settings._profiles.at(0).GetName()); + VERIFY_ARE_EQUAL(L"profile1", settings._profiles.at(1).GetName()); + VERIFY_ARE_EQUAL(L"profile4", settings._profiles.at(2).GetName()); + VERIFY_ARE_EQUAL(L"profile6", settings._profiles.at(3).GetName()); } } @@ -348,6 +374,10 @@ namespace TerminalAppLocalTests } ] })" }; + Profile profile4{ Microsoft::Console::Utils::GuidFromString(L"{6239a42c-4444-49a3-80bd-e8fdd045185c}") }; + profile4._name = L"profile4"; + Profile profile5{ Microsoft::Console::Utils::GuidFromString(L"{6239a42c-4444-49a3-80bd-e8fdd045185c}") }; + profile5._name = L"profile5"; // Case 2: Bad settings Log::Comment(NoThrowString().Format( @@ -355,14 +385,573 @@ namespace TerminalAppLocalTests const auto settingsObject = VerifyParseSucceeded(badProfiles); auto settings = CascadiaSettings::FromJson(settingsObject); + settings->_profiles.push_back(profile4); + settings->_profiles.push_back(profile5); + settings->_ValidateSettings(); - VERIFY_ARE_EQUAL(static_cast(2), settings->_warnings.size()); + VERIFY_ARE_EQUAL(2u, settings->_warnings.size()); VERIFY_ARE_EQUAL(::TerminalApp::SettingsLoadWarnings::DuplicateProfile, settings->_warnings.at(0)); VERIFY_ARE_EQUAL(::TerminalApp::SettingsLoadWarnings::MissingDefaultProfile, settings->_warnings.at(1)); - VERIFY_ARE_EQUAL(static_cast(2), settings->_profiles.size()); + VERIFY_ARE_EQUAL(3u, settings->_profiles.size()); VERIFY_ARE_EQUAL(settings->_globals.GetDefaultProfile(), settings->_profiles.at(0).GetGuid()); + VERIFY_IS_TRUE(settings->_profiles.at(0)._guid.has_value()); + VERIFY_IS_TRUE(settings->_profiles.at(1)._guid.has_value()); + VERIFY_IS_TRUE(settings->_profiles.at(2)._guid.has_value()); + } + + void SettingsTests::LayerGlobalProperties() + { + const std::string settings0String{ R"( + { + "globals": { + "alwaysShowTabs": true, + "initialCols" : 120, + "initialRows" : 30 + } + })" }; + const std::string settings1String{ R"( + { + "globals": { + "showTabsInTitlebar": false, + "initialCols" : 240, + "initialRows" : 60 + } + })" }; + const auto settings0Json = VerifyParseSucceeded(settings0String); + const auto settings1Json = VerifyParseSucceeded(settings1String); + + CascadiaSettings settings; + + settings.LayerJson(settings0Json); + VERIFY_ARE_EQUAL(true, settings._globals._alwaysShowTabs); + VERIFY_ARE_EQUAL(120, settings._globals._initialCols); + VERIFY_ARE_EQUAL(30, settings._globals._initialRows); + VERIFY_ARE_EQUAL(true, settings._globals._showTabsInTitlebar); + + settings.LayerJson(settings1Json); + VERIFY_ARE_EQUAL(true, settings._globals._alwaysShowTabs); + VERIFY_ARE_EQUAL(240, settings._globals._initialCols); + VERIFY_ARE_EQUAL(60, settings._globals._initialRows); + VERIFY_ARE_EQUAL(false, settings._globals._showTabsInTitlebar); + } + + void SettingsTests::ValidateProfileOrdering() + { + const std::string userProfiles0String{ R"( + { + "profiles": [ + { + "name" : "profile0", + "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}" + }, + { + "name" : "profile1", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}" + } + ] + })" }; + + const std::string defaultProfilesString{ R"( + { + "profiles": [ + { + "name" : "profile2", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}" + }, + { + "name" : "profile3", + "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}" + } + ] + })" }; + + const std::string userProfiles1String{ R"( + { + "profiles": [ + { + "name" : "profile4", + "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}" + }, + { + "name" : "profile5", + "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}" + } + ] + })" }; + + const auto userProfiles0Json = VerifyParseSucceeded(userProfiles0String); + const auto userProfiles1Json = VerifyParseSucceeded(userProfiles1String); + const auto defaultProfilesJson = VerifyParseSucceeded(defaultProfilesString); + + { + Log::Comment(NoThrowString().Format( + L"Case 1: Simple swapping of the ordering. The user has the " + L"default profiles in the opposite order of the default ordering.")); + + CascadiaSettings settings; + settings._LayerJsonString(defaultProfilesString, true); + VERIFY_ARE_EQUAL(2u, settings._profiles.size()); + VERIFY_ARE_EQUAL(L"profile2", settings._profiles.at(0)._name); + VERIFY_ARE_EQUAL(L"profile3", settings._profiles.at(1)._name); + + settings._LayerJsonString(userProfiles0String, false); + VERIFY_ARE_EQUAL(2u, settings._profiles.size()); + VERIFY_ARE_EQUAL(L"profile1", settings._profiles.at(0)._name); + VERIFY_ARE_EQUAL(L"profile0", settings._profiles.at(1)._name); + + settings._ReorderProfilesToMatchUserSettingsOrder(); + VERIFY_ARE_EQUAL(2u, settings._profiles.size()); + VERIFY_ARE_EQUAL(L"profile0", settings._profiles.at(0)._name); + VERIFY_ARE_EQUAL(L"profile1", settings._profiles.at(1)._name); + } + + { + Log::Comment(NoThrowString().Format( + L"Case 2: Make sure all the user's profiles appear before the defaults.")); + + CascadiaSettings settings; + settings._LayerJsonString(defaultProfilesString, true); + VERIFY_ARE_EQUAL(2u, settings._profiles.size()); + VERIFY_ARE_EQUAL(L"profile2", settings._profiles.at(0)._name); + VERIFY_ARE_EQUAL(L"profile3", settings._profiles.at(1)._name); + + settings._LayerJsonString(userProfiles1String, false); + VERIFY_ARE_EQUAL(3u, settings._profiles.size()); + VERIFY_ARE_EQUAL(L"profile2", settings._profiles.at(0)._name); + VERIFY_ARE_EQUAL(L"profile4", settings._profiles.at(1)._name); + VERIFY_ARE_EQUAL(L"profile5", settings._profiles.at(2)._name); + + settings._ReorderProfilesToMatchUserSettingsOrder(); + VERIFY_ARE_EQUAL(3u, settings._profiles.size()); + VERIFY_ARE_EQUAL(L"profile4", settings._profiles.at(0)._name); + VERIFY_ARE_EQUAL(L"profile5", settings._profiles.at(1)._name); + VERIFY_ARE_EQUAL(L"profile2", settings._profiles.at(2)._name); + } + } + + void SettingsTests::ValidateHideProfiles() + { + const std::string defaultProfilesString{ R"( + { + "profiles": [ + { + "name" : "profile2", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}" + }, + { + "name" : "profile3", + "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}" + } + ] + })" }; + + const std::string userProfiles0String{ R"( + { + "profiles": [ + { + "name" : "profile0", + "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "hidden": true + }, + { + "name" : "profile1", + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}" + } + ] + })" }; + + const std::string userProfiles1String{ R"( + { + "profiles": [ + { + "name" : "profile4", + "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", + "hidden": true + }, + { + "name" : "profile5", + "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}" + }, + { + "name" : "profile6", + "guid": "{6239a42c-3333-49a3-80bd-e8fdd045185c}", + "hidden": true + } + ] + })" }; + + const auto userProfiles0Json = VerifyParseSucceeded(userProfiles0String); + const auto userProfiles1Json = VerifyParseSucceeded(userProfiles1String); + const auto defaultProfilesJson = VerifyParseSucceeded(defaultProfilesString); + + { + CascadiaSettings settings; + settings._LayerJsonString(defaultProfilesString, true); + VERIFY_ARE_EQUAL(2u, settings._profiles.size()); + VERIFY_ARE_EQUAL(L"profile2", settings._profiles.at(0)._name); + VERIFY_ARE_EQUAL(L"profile3", settings._profiles.at(1)._name); + VERIFY_ARE_EQUAL(false, settings._profiles.at(0)._hidden); + VERIFY_ARE_EQUAL(false, settings._profiles.at(1)._hidden); + + settings._LayerJsonString(userProfiles0String, false); + VERIFY_ARE_EQUAL(2u, settings._profiles.size()); + VERIFY_ARE_EQUAL(L"profile1", settings._profiles.at(0)._name); + VERIFY_ARE_EQUAL(L"profile0", settings._profiles.at(1)._name); + VERIFY_ARE_EQUAL(false, settings._profiles.at(0)._hidden); + VERIFY_ARE_EQUAL(true, settings._profiles.at(1)._hidden); + + settings._ReorderProfilesToMatchUserSettingsOrder(); + settings._RemoveHiddenProfiles(); + VERIFY_ARE_EQUAL(1u, settings._profiles.size()); + VERIFY_ARE_EQUAL(L"profile1", settings._profiles.at(0)._name); + VERIFY_ARE_EQUAL(false, settings._profiles.at(0)._hidden); + } + + { + CascadiaSettings settings; + settings._LayerJsonString(defaultProfilesString, true); + VERIFY_ARE_EQUAL(2u, settings._profiles.size()); + VERIFY_ARE_EQUAL(L"profile2", settings._profiles.at(0)._name); + VERIFY_ARE_EQUAL(L"profile3", settings._profiles.at(1)._name); + VERIFY_ARE_EQUAL(false, settings._profiles.at(0)._hidden); + VERIFY_ARE_EQUAL(false, settings._profiles.at(1)._hidden); + + settings._LayerJsonString(userProfiles1String, false); + VERIFY_ARE_EQUAL(4u, settings._profiles.size()); + VERIFY_ARE_EQUAL(L"profile2", settings._profiles.at(0)._name); + VERIFY_ARE_EQUAL(L"profile4", settings._profiles.at(1)._name); + VERIFY_ARE_EQUAL(L"profile5", settings._profiles.at(2)._name); + VERIFY_ARE_EQUAL(L"profile6", settings._profiles.at(3)._name); + VERIFY_ARE_EQUAL(false, settings._profiles.at(0)._hidden); + VERIFY_ARE_EQUAL(true, settings._profiles.at(1)._hidden); + VERIFY_ARE_EQUAL(false, settings._profiles.at(2)._hidden); + VERIFY_ARE_EQUAL(true, settings._profiles.at(3)._hidden); + + settings._ReorderProfilesToMatchUserSettingsOrder(); + settings._RemoveHiddenProfiles(); + VERIFY_ARE_EQUAL(2u, settings._profiles.size()); + VERIFY_ARE_EQUAL(L"profile5", settings._profiles.at(0)._name); + VERIFY_ARE_EQUAL(L"profile2", settings._profiles.at(1)._name); + VERIFY_ARE_EQUAL(false, settings._profiles.at(0)._hidden); + VERIFY_ARE_EQUAL(false, settings._profiles.at(1)._hidden); + } } + void SettingsTests::ValidateProfilesGenerateGuids() + { + const std::string profile0String{ R"( + { + "name" : "profile0" + })" }; + const std::string profile1String{ R"( + { + "name" : "profile1" + })" }; + const std::string profile2String{ R"( + { + "name" : "profile2", + "guid" : null + })" }; + const std::string profile3String{ R"( + { + "name" : "profile3", + "guid" : "{00000000-0000-0000-0000-000000000000}" + })" }; + const std::string profile4String{ R"( + { + "name" : "profile4", + "guid" : "{6239a42c-1de4-49a3-80bd-e8fdd045185c}" + })" }; + const std::string profile5String{ R"( + { + "name" : "profile2" + })" }; + + const auto profile0Json = VerifyParseSucceeded(profile0String); + const auto profile1Json = VerifyParseSucceeded(profile1String); + const auto profile2Json = VerifyParseSucceeded(profile2String); + const auto profile3Json = VerifyParseSucceeded(profile3String); + const auto profile4Json = VerifyParseSucceeded(profile4String); + const auto profile5Json = VerifyParseSucceeded(profile5String); + + const auto profile0 = Profile::FromJson(profile0Json); + const auto profile1 = Profile::FromJson(profile1Json); + const auto profile2 = Profile::FromJson(profile2Json); + const auto profile3 = Profile::FromJson(profile3Json); + const auto profile4 = Profile::FromJson(profile4Json); + const auto profile5 = Profile::FromJson(profile5Json); + + const GUID cmdGuid = Utils::GuidFromString(L"{6239a42c-1de4-49a3-80bd-e8fdd045185c}"); + const GUID nullGuid{ 0 }; + + VERIFY_IS_FALSE(profile0._guid.has_value()); + VERIFY_IS_FALSE(profile1._guid.has_value()); + VERIFY_IS_FALSE(profile2._guid.has_value()); + VERIFY_IS_TRUE(profile3._guid.has_value()); + VERIFY_IS_TRUE(profile4._guid.has_value()); + VERIFY_IS_FALSE(profile5._guid.has_value()); + + VERIFY_ARE_EQUAL(profile3.GetGuid(), nullGuid); + VERIFY_ARE_EQUAL(profile4.GetGuid(), cmdGuid); + + CascadiaSettings settings; + settings._profiles.emplace_back(profile0); + settings._profiles.emplace_back(profile1); + settings._profiles.emplace_back(profile2); + settings._profiles.emplace_back(profile3); + settings._profiles.emplace_back(profile4); + settings._profiles.emplace_back(profile5); + + settings._ValidateProfilesHaveGuid(); + VERIFY_IS_TRUE(settings._profiles.at(0)._guid.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(1)._guid.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(2)._guid.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(3)._guid.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(4)._guid.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(5)._guid.has_value()); + + VERIFY_ARE_NOT_EQUAL(settings._profiles.at(0).GetGuid(), nullGuid); + VERIFY_ARE_NOT_EQUAL(settings._profiles.at(1).GetGuid(), nullGuid); + VERIFY_ARE_NOT_EQUAL(settings._profiles.at(2).GetGuid(), nullGuid); + VERIFY_ARE_EQUAL(settings._profiles.at(3).GetGuid(), nullGuid); + VERIFY_ARE_NOT_EQUAL(settings._profiles.at(4).GetGuid(), nullGuid); + VERIFY_ARE_NOT_EQUAL(settings._profiles.at(5).GetGuid(), nullGuid); + + VERIFY_ARE_NOT_EQUAL(settings._profiles.at(0).GetGuid(), cmdGuid); + VERIFY_ARE_NOT_EQUAL(settings._profiles.at(1).GetGuid(), cmdGuid); + VERIFY_ARE_NOT_EQUAL(settings._profiles.at(2).GetGuid(), cmdGuid); + VERIFY_ARE_NOT_EQUAL(settings._profiles.at(3).GetGuid(), cmdGuid); + VERIFY_ARE_EQUAL(settings._profiles.at(4).GetGuid(), cmdGuid); + VERIFY_ARE_NOT_EQUAL(settings._profiles.at(5).GetGuid(), cmdGuid); + + VERIFY_ARE_NOT_EQUAL(settings._profiles.at(0).GetGuid(), settings._profiles.at(2).GetGuid()); + VERIFY_ARE_NOT_EQUAL(settings._profiles.at(1).GetGuid(), settings._profiles.at(2).GetGuid()); + VERIFY_ARE_EQUAL(settings._profiles.at(2).GetGuid(), settings._profiles.at(2).GetGuid()); + VERIFY_ARE_NOT_EQUAL(settings._profiles.at(3).GetGuid(), settings._profiles.at(2).GetGuid()); + VERIFY_ARE_NOT_EQUAL(settings._profiles.at(4).GetGuid(), settings._profiles.at(2).GetGuid()); + VERIFY_ARE_EQUAL(settings._profiles.at(5).GetGuid(), settings._profiles.at(2).GetGuid()); + } + + void SettingsTests::GeneratedGuidRoundtrips() + { + // Parse a profile without a guid. + // We should automatically generate a GUID for that profile. + // When that profile is serialized and deserialized again, the GUID we + // generated for it should persist. + const std::string profileWithoutGuid{ R"({ + "name" : "profile0" + })" }; + const auto profile0Json = VerifyParseSucceeded(profileWithoutGuid); + + const auto profile0 = Profile::FromJson(profile0Json); + const GUID nullGuid{ 0 }; + + VERIFY_IS_FALSE(profile0._guid.has_value()); + + const auto serialized0Profile = profile0.ToJson(); + const auto profile1 = Profile::FromJson(serialized0Profile); + VERIFY_IS_FALSE(profile0._guid.has_value()); + VERIFY_ARE_EQUAL(profile1._guid.has_value(), profile0._guid.has_value()); + + CascadiaSettings settings; + settings._profiles.emplace_back(profile1); + settings._ValidateProfilesHaveGuid(); + + VERIFY_IS_TRUE(settings._profiles.at(0)._guid.has_value()); + + const auto serialized1Profile = settings._profiles.at(0).ToJson(); + + const auto profile2 = Profile::FromJson(serialized1Profile); + VERIFY_IS_TRUE(settings._profiles.at(0)._guid.has_value()); + VERIFY_ARE_EQUAL(settings._profiles.at(0)._guid.has_value(), profile2._guid.has_value()); + VERIFY_ARE_EQUAL(settings._profiles.at(0).GetGuid(), profile2.GetGuid()); + } + + void SettingsTests::TestAllValidationsWithNullGuids() + { + const std::string settings0String{ R"( + { + "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "profiles": [ + { + "name" : "profile0", + "guid" : "{6239a42c-1111-49a3-80bd-e8fdd045185c}" + }, + { + "name" : "profile1" + } + ] + })" }; + + const auto settings0Json = VerifyParseSucceeded(settings0String); + + CascadiaSettings settings; + settings._LayerJsonString(settings0String, false); + + VERIFY_ARE_EQUAL(2u, settings._profiles.size()); + VERIFY_IS_TRUE(settings._profiles.at(0)._guid.has_value()); + VERIFY_IS_FALSE(settings._profiles.at(1)._guid.has_value()); + + settings._ValidateSettings(); + VERIFY_ARE_EQUAL(0u, settings._warnings.size()); + VERIFY_ARE_EQUAL(2u, settings._profiles.size()); + VERIFY_IS_TRUE(settings._profiles.at(0)._guid.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(1)._guid.has_value()); + } + + void SettingsTests::TestReorderWithNullGuids() + { + const std::string settings0String{ R"( + { + "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "profiles": [ + { + "name" : "profile0", + "guid" : "{6239a42c-1111-49a3-80bd-e8fdd045185c}" + }, + { + "name" : "profile1" + }, + { + "name" : "cmdFromUserSettings", + "guid" : "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}" // from defaults.json + } + ] + })" }; + + const auto settings0Json = VerifyParseSucceeded(settings0String); + + CascadiaSettings settings; + settings._LayerJsonString(DefaultJson, true); + VERIFY_ARE_EQUAL(2u, settings._profiles.size()); + VERIFY_IS_TRUE(settings._profiles.at(0)._guid.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(1)._guid.has_value()); + VERIFY_ARE_EQUAL(L"Windows PowerShell", settings._profiles.at(0)._name); + VERIFY_ARE_EQUAL(L"cmd", settings._profiles.at(1)._name); + + settings._LayerJsonString(settings0String, false); + + VERIFY_ARE_EQUAL(4u, settings._profiles.size()); + VERIFY_IS_TRUE(settings._profiles.at(0)._guid.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(1)._guid.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(2)._guid.has_value()); + VERIFY_IS_FALSE(settings._profiles.at(3)._guid.has_value()); + VERIFY_ARE_EQUAL(L"Windows PowerShell", settings._profiles.at(0)._name); + VERIFY_ARE_EQUAL(L"cmdFromUserSettings", settings._profiles.at(1)._name); + VERIFY_ARE_EQUAL(L"profile0", settings._profiles.at(2)._name); + VERIFY_ARE_EQUAL(L"profile1", settings._profiles.at(3)._name); + + settings._ValidateSettings(); + VERIFY_ARE_EQUAL(0u, settings._warnings.size()); + VERIFY_ARE_EQUAL(4u, settings._profiles.size()); + VERIFY_IS_TRUE(settings._profiles.at(0)._guid.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(1)._guid.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(2)._guid.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(3)._guid.has_value()); + VERIFY_ARE_EQUAL(L"profile0", settings._profiles.at(0)._name); + VERIFY_ARE_EQUAL(L"profile1", settings._profiles.at(1)._name); + VERIFY_ARE_EQUAL(L"cmdFromUserSettings", settings._profiles.at(2)._name); + VERIFY_ARE_EQUAL(L"Windows PowerShell", settings._profiles.at(3)._name); + } + + void SettingsTests::TestReorderingWithoutGuid() + { + Log::Comment(NoThrowString().Format( + L"During the GH#2515 PR, this set of settings was found to cause an" + L" exception, crashing the terminal. This test ensures that it doesn't.")); + + Log::Comment(NoThrowString().Format( + L"While similar to TestReorderWithNullGuids, there's something else" + L" about this scenario specifically that causes a crash, when " + L" TestReorderWithNullGuids did _not_.")); + + const std::string settings0String{ R"( + { + "defaultProfile" : "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}", + "profiles": [ + { + "guid" : "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}", + "acrylicOpacity" : 0.5, + "closeOnExit" : true, + "background" : "#8A00FF", + "foreground" : "#F2F2F2", + "commandline" : "cmd.exe", + "cursorColor" : "#FFFFFF", + "fontFace" : "Cascadia Code", + "fontSize" : 10, + "historySize" : 9001, + "padding" : "20", + "snapOnInput" : true, + "startingDirectory" : "%USERPROFILE%", + "useAcrylic" : true + }, + { + "name" : "ThisProfileShouldNotCrash", + "tabTitle" : "Ubuntu", + "acrylicOpacity" : 0.5, + "background" : "#2C001E", + "closeOnExit" : true, + "colorScheme" : "Campbell", + "commandline" : "wsl.exe", + "cursorColor" : "#FFFFFF", + "cursorShape" : "bar", + "fontSize" : 10, + "historySize" : 9001, + "padding" : "0, 0, 0, 0", + "snapOnInput" : true, + "useAcrylic" : true + }, + { + // This is the same profile that would be generated by the WSL profile generator. + "name" : "Ubuntu", + "guid" : "{2C4DE342-38B7-51CF-B940-2309A097F518}", + "acrylicOpacity" : 0.5, + "background" : "#2C001E", + "closeOnExit" : false, + "cursorColor" : "#FFFFFF", + "cursorShape" : "bar", + "fontSize" : 10, + "historySize" : 9001, + "snapOnInput" : true, + "useAcrylic" : true + } + ] + })" }; + + const auto settings0Json = VerifyParseSucceeded(settings0String); + + CascadiaSettings settings; + settings._LayerJsonString(DefaultJson, true); + VERIFY_ARE_EQUAL(2u, settings._profiles.size()); + VERIFY_IS_TRUE(settings._profiles.at(0)._guid.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(1)._guid.has_value()); + VERIFY_ARE_EQUAL(L"Windows PowerShell", settings._profiles.at(0)._name); + VERIFY_ARE_EQUAL(L"cmd", settings._profiles.at(1)._name); + + settings._LayerJsonString(settings0String, false); + + VERIFY_ARE_EQUAL(4u, settings._profiles.size()); + VERIFY_IS_TRUE(settings._profiles.at(0)._guid.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(1)._guid.has_value()); + VERIFY_IS_FALSE(settings._profiles.at(2)._guid.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(3)._guid.has_value()); + VERIFY_ARE_EQUAL(L"Windows PowerShell", settings._profiles.at(0)._name); + VERIFY_ARE_EQUAL(L"cmd", settings._profiles.at(1)._name); + VERIFY_ARE_EQUAL(L"ThisProfileShouldNotCrash", settings._profiles.at(2)._name); + VERIFY_ARE_EQUAL(L"Ubuntu", settings._profiles.at(3)._name); + + settings._ValidateSettings(); + VERIFY_ARE_EQUAL(0u, settings._warnings.size()); + VERIFY_ARE_EQUAL(4u, settings._profiles.size()); + VERIFY_IS_TRUE(settings._profiles.at(0)._guid.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(1)._guid.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(2)._guid.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(3)._guid.has_value()); + VERIFY_ARE_EQUAL(L"cmd", settings._profiles.at(0)._name); + VERIFY_ARE_EQUAL(L"ThisProfileShouldNotCrash", settings._profiles.at(1)._name); + VERIFY_ARE_EQUAL(L"Ubuntu", settings._profiles.at(2)._name); + VERIFY_ARE_EQUAL(L"Windows PowerShell", settings._profiles.at(3)._name); + } } diff --git a/src/cascadia/LocalTests_TerminalApp/TabTests.cpp b/src/cascadia/LocalTests_TerminalApp/TabTests.cpp index 4a1f692139e..bb7853061ff 100644 --- a/src/cascadia/LocalTests_TerminalApp/TabTests.cpp +++ b/src/cascadia/LocalTests_TerminalApp/TabTests.cpp @@ -69,7 +69,7 @@ namespace TerminalAppLocalTests { // Verify we can create a WinRT type we authored // Just creating it is enough to know that everything is working. - winrt::Microsoft::Terminal::Settings::TerminalSettings settings{}; + winrt::Microsoft::Terminal::Settings::TerminalSettings settings; VERIFY_IS_NOT_NULL(settings); auto oldFontSize = settings.FontSize(); settings.FontSize(oldFontSize + 5); diff --git a/src/cascadia/LocalTests_TerminalApp/TerminalApp.LocalTests.vcxproj b/src/cascadia/LocalTests_TerminalApp/TerminalApp.LocalTests.vcxproj index 554737e67e8..c24da27a8b7 100644 --- a/src/cascadia/LocalTests_TerminalApp/TerminalApp.LocalTests.vcxproj +++ b/src/cascadia/LocalTests_TerminalApp/TerminalApp.LocalTests.vcxproj @@ -6,11 +6,15 @@ + + + + Create @@ -127,7 +131,7 @@ Outputs="$(OutDir)$(TargetName).AppxManifest.xml" DependsOnTargets="_LocalTestsGenerateCombinedManifests"> - + diff --git a/src/cascadia/TerminalApp/App.cpp b/src/cascadia/TerminalApp/App.cpp index dc156bef5c5..5311f65fed7 100644 --- a/src/cascadia/TerminalApp/App.cpp +++ b/src/cascadia/TerminalApp/App.cpp @@ -413,8 +413,7 @@ namespace winrt::TerminalApp::implementation if (FAILED(_settingsLoadedResult)) { - _settings = std::make_unique(); - _settings->CreateDefaults(); + _settings = CascadiaSettings::LoadDefaults(); } auto end = std::chrono::high_resolution_clock::now(); diff --git a/src/cascadia/TerminalApp/App.h b/src/cascadia/TerminalApp/App.h index 8d901b756e4..1973fe5de73 100644 --- a/src/cascadia/TerminalApp/App.h +++ b/src/cascadia/TerminalApp/App.h @@ -75,7 +75,6 @@ namespace winrt::TerminalApp::implementation [[nodiscard]] HRESULT _TryLoadSettings() noexcept; void _LoadSettings(); - void _OpenSettings(); void _RegisterSettingsChange(); fire_and_forget _DispatchReloadSettings(); void _ReloadSettings(); diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index 1abce6291d3..a862a5cd787 100644 --- a/src/cascadia/TerminalApp/AppActionHandlers.cpp +++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp @@ -119,7 +119,8 @@ namespace winrt::TerminalApp::implementation void TerminalPage::_HandleOpenSettings(const IInspectable& /*sender*/, const TerminalApp::ActionEventArgs& args) { - _OpenSettings(); + // TODO:GH#2557 Add an optional arg for opening the defaults here + _LaunchSettings(false); args.Handled(true); } diff --git a/src/cascadia/TerminalApp/AppKeyBindings.cpp b/src/cascadia/TerminalApp/AppKeyBindings.cpp index 7e35e139eac..58693ad9042 100644 --- a/src/cascadia/TerminalApp/AppKeyBindings.cpp +++ b/src/cascadia/TerminalApp/AppKeyBindings.cpp @@ -18,6 +18,17 @@ namespace winrt::TerminalApp::implementation _keyShortcuts[chord] = action; } + // Method Description: + // - Remove the action that's bound to a particular KeyChord. + // Arguments: + // - chord: the keystroke to remove the action for. + // Return Value: + // - + void AppKeyBindings::ClearKeyBinding(const Settings::KeyChord& chord) + { + _keyShortcuts.erase(chord); + } + Microsoft::Terminal::Settings::KeyChord AppKeyBindings::GetKeyBinding(TerminalApp::ShortcutAction const& action) { for (auto& kv : _keyShortcuts) diff --git a/src/cascadia/TerminalApp/AppKeyBindings.h b/src/cascadia/TerminalApp/AppKeyBindings.h index 0ad405378ef..7240516770d 100644 --- a/src/cascadia/TerminalApp/AppKeyBindings.h +++ b/src/cascadia/TerminalApp/AppKeyBindings.h @@ -7,6 +7,13 @@ #include "ActionArgs.h" #include "..\inc\cppwinrt_utils.h" +// fwdecl unittest classes +namespace TerminalAppLocalTests +{ + class SettingsTests; + class KeyBindingsTests; +} + namespace winrt::TerminalApp::implementation { struct KeyChordHash @@ -35,10 +42,15 @@ namespace winrt::TerminalApp::implementation bool TryKeyChord(winrt::Microsoft::Terminal::Settings::KeyChord const& kc); void SetKeyBinding(TerminalApp::ShortcutAction const& action, winrt::Microsoft::Terminal::Settings::KeyChord const& chord); + void ClearKeyBinding(winrt::Microsoft::Terminal::Settings::KeyChord const& chord); Microsoft::Terminal::Settings::KeyChord GetKeyBinding(TerminalApp::ShortcutAction const& action); static Windows::System::VirtualKeyModifiers ConvertVKModifiers(winrt::Microsoft::Terminal::Settings::KeyModifiers modifiers); + // Defined in AppKeyBindingsSerialization.cpp + void LayerJson(const Json::Value& json); + Json::Value ToJson(); + // clang-format off TYPED_EVENT(CopyText, TerminalApp::AppKeyBindings, TerminalApp::ActionEventArgs); TYPED_EVENT(PasteText, TerminalApp::AppKeyBindings, TerminalApp::ActionEventArgs); @@ -69,6 +81,9 @@ namespace winrt::TerminalApp::implementation private: std::unordered_map _keyShortcuts; bool _DoAction(ShortcutAction action); + + friend class TerminalAppLocalTests::SettingsTests; + friend class TerminalAppLocalTests::KeyBindingsTests; }; } diff --git a/src/cascadia/TerminalApp/AppKeyBindings.idl b/src/cascadia/TerminalApp/AppKeyBindings.idl index 8786d44ab17..2cf38130bc7 100644 --- a/src/cascadia/TerminalApp/AppKeyBindings.idl +++ b/src/cascadia/TerminalApp/AppKeyBindings.idl @@ -6,7 +6,8 @@ namespace TerminalApp { enum ShortcutAction { - CopyText = 0, + Invalid = 0, + CopyText, CopyTextWithoutNewlines, PasteText, NewTab, @@ -60,6 +61,7 @@ namespace TerminalApp AppKeyBindings(); void SetKeyBinding(ShortcutAction action, Microsoft.Terminal.Settings.KeyChord chord); + void ClearKeyBinding(Microsoft.Terminal.Settings.KeyChord chord); Microsoft.Terminal.Settings.KeyChord GetKeyBinding(ShortcutAction action); event Windows.Foundation.TypedEventHandler CopyText; diff --git a/src/cascadia/TerminalApp/AppKeyBindingsSerialization.cpp b/src/cascadia/TerminalApp/AppKeyBindingsSerialization.cpp index 4e0e752976c..426fd406669 100644 --- a/src/cascadia/TerminalApp/AppKeyBindingsSerialization.cpp +++ b/src/cascadia/TerminalApp/AppKeyBindingsSerialization.cpp @@ -1,10 +1,16 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +// - A couple helper functions for serializing/deserializing an AppKeyBindings +// to/from json. +// +// Author(s): +// - Mike Griese - May 2019 #include "pch.h" -#include "AppKeyBindingsSerialization.h" +#include "AppKeyBindings.h" #include "KeyChordSerialization.h" #include "Utils.h" +#include "JsonUtils.h" #include using namespace winrt::Microsoft::Terminal::Settings; @@ -13,6 +19,9 @@ using namespace winrt::TerminalApp; static constexpr std::string_view KeysKey{ "keys" }; static constexpr std::string_view CommandKey{ "command" }; +// This key is reserved to remove a keybinding, instead of mapping it to an action. +static constexpr std::string_view UnboundKey{ "unbound" }; + static constexpr std::string_view CopyTextKey{ "copy" }; static constexpr std::string_view CopyTextWithoutNewlinesKey{ "copyTextWithoutNewlines" }; static constexpr std::string_view PasteTextKey{ "paste" }; @@ -118,6 +127,7 @@ static const std::map> commandName { MoveFocusUpKey, ShortcutAction::MoveFocusUp }, { MoveFocusDownKey, ShortcutAction::MoveFocusDown }, { OpenSettingsKey, ShortcutAction::OpenSettings }, + { UnboundKey, ShortcutAction::Invalid }, }; // Function Description: @@ -157,7 +167,7 @@ static Json::Value _ShortcutAsJsonObject(const KeyChord& chord, // ShortcutAction. // Return Value: // - a Json::Value which is an equivalent serialization of this object. -Json::Value AppKeyBindingsSerialization::ToJson(const winrt::TerminalApp::AppKeyBindings& bindings) +Json::Value winrt::TerminalApp::implementation::AppKeyBindings::ToJson() { Json::Value bindingsArray; @@ -168,7 +178,7 @@ Json::Value AppKeyBindingsSerialization::ToJson(const winrt::TerminalApp::AppKey const auto searchedForName = actionName.first; const auto searchedForAction = actionName.second; - if (const auto chord{ bindings.GetKeyBinding(searchedForAction) }) + if (const auto chord{ GetKeyBinding(searchedForAction) }) { if (const auto serialization{ _ShortcutAsJsonObject(chord, searchedForName) }) { @@ -187,53 +197,71 @@ Json::Value AppKeyBindingsSerialization::ToJson(const winrt::TerminalApp::AppKey // listed in `commandNames`, and `keys` is an array of keypresses. Currently, // the array should contain a single string, which can be deserialized into a // KeyChord. +// - Applies the deserialized keybindings to the provided `bindings` object. If +// a key chord in `json` is already bound to an action, that chord will be +// overwritten with the new action. If a chord is bound to `null` or +// `"unbound"`, then we'll clear the keybinding from the existing keybindings. // Arguments: // - json: and array of JsonObject's to deserialize into our _keyShortcuts mapping. -// Return Value: -// - the newly constructed AppKeyBindings object. -winrt::TerminalApp::AppKeyBindings AppKeyBindingsSerialization::FromJson(const Json::Value& json) +void winrt::TerminalApp::implementation::AppKeyBindings::LayerJson(const Json::Value& json) { - winrt::TerminalApp::AppKeyBindings newBindings{}; - for (const auto& value : json) { - if (value.isObject()) + if (!value.isObject()) { - const auto commandString = value[JsonKey(CommandKey)]; - const auto keys = value[JsonKey(KeysKey)]; + continue; + } + + const auto commandVal = value[JsonKey(CommandKey)]; + const auto keys = value[JsonKey(KeysKey)]; - if (commandString && keys) + if (keys) + { + if (!keys.isArray() || keys.size() != 1) { - if (!keys.isArray() || keys.size() != 1) - { - continue; - } - const auto keyChordString = winrt::to_hstring(keys[0].asString()); - ShortcutAction action; + continue; + } + const auto keyChordString = winrt::to_hstring(keys[0].asString()); + // Invalid is our placeholder that the action was not parsed. + ShortcutAction action = ShortcutAction::Invalid; + + // Only try to parse the action if it's actually a string value. + // `null` will not pass this check. + if (commandVal.isString()) + { + auto commandString = commandVal.asString(); - // Try matching the command to one we have - const auto found = commandNames.find(commandString.asString()); + // Try matching the command to one we have. If we can't find the + // action name in our list of names, let's just unbind that key. + const auto found = commandNames.find(commandString); if (found != commandNames.end()) { action = found->second; } - else - { - continue; - } + } - // Try parsing the chord - try + // Try parsing the chord + try + { + const auto chord = KeyChordSerialization::FromString(keyChordString); + + // If we couldn't find the action they want to set the chord to, + // or the action was `null` or `"unbound"`, just clear out the + // keybinding. Otherwise, set the keybinding to the action we + // found. + if (action != ShortcutAction::Invalid) { - const auto chord = KeyChordSerialization::FromString(keyChordString); - newBindings.SetKeyBinding(action, chord); + SetKeyBinding(action, chord); } - catch (...) + else { - continue; + ClearKeyBinding(chord); } } + catch (...) + { + continue; + } } } - return newBindings; } diff --git a/src/cascadia/TerminalApp/AppKeyBindingsSerialization.h b/src/cascadia/TerminalApp/AppKeyBindingsSerialization.h deleted file mode 100644 index 2f1c479c3bb..00000000000 --- a/src/cascadia/TerminalApp/AppKeyBindingsSerialization.h +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// -// Module Name: -// - AppKeyBindingsSerialization.h -// -// Abstract: -// - A couple helper functions for serializing/deserializing an AppKeyBindings -// to/from json. We need this to exist as external helper functions, rather -// than defining these as methods on the AppKeyBindings class, because -// AppKeyBindings is a winrt type. When we're working with a AppKeyBindings -// object, we only have access to methods defined on the winrt interface (in -// the idl). We don't have access to methods we define on the -// implementation. Since JsonValue is not a winrt type, we can't define any -// methods that operate on it in the idl. -// -// Author(s): -// - Mike Griese - May 2019 - -#pragma once -#include "AppKeyBindings.h" - -class AppKeyBindingsSerialization final -{ -public: - static winrt::TerminalApp::AppKeyBindings FromJson(const Json::Value& json); - static Json::Value ToJson(const winrt::TerminalApp::AppKeyBindings& bindings); -}; diff --git a/src/cascadia/TerminalApp/CascadiaSettings.cpp b/src/cascadia/TerminalApp/CascadiaSettings.cpp index 3a98b5c9b85..3d8a786f2f1 100644 --- a/src/cascadia/TerminalApp/CascadiaSettings.cpp +++ b/src/cascadia/TerminalApp/CascadiaSettings.cpp @@ -9,7 +9,7 @@ #include "CascadiaSettings.h" #include "../../types/inc/utils.hpp" #include "../../inc/DefaultSettings.h" -#include "winrt/Microsoft.Terminal.TerminalConnection.h" +#include "Utils.h" using namespace winrt::Microsoft::Terminal::Settings; using namespace ::TerminalApp; @@ -22,6 +22,7 @@ using namespace Microsoft::Console; static constexpr GUID TERMINAL_PROFILE_NAMESPACE_GUID = { 0x2bde4a90, 0xd05f, 0x401c, { 0x94, 0x92, 0xe4, 0x8, 0x84, 0xea, 0xd1, 0xd8 } }; static constexpr std::wstring_view PACKAGED_PROFILE_ICON_PATH{ L"ms-appx:///ProfileIcons/" }; + static constexpr std::wstring_view PACKAGED_PROFILE_ICON_EXTENSION{ L".png" }; static constexpr std::wstring_view DEFAULT_LINUX_ICON_GUID{ L"{9acb9455-ca41-5af7-950f-6bca1bc9722f}" }; @@ -35,187 +36,6 @@ CascadiaSettings::~CascadiaSettings() { } -ColorScheme _CreateCampbellScheme() -{ - ColorScheme campbellScheme{ L"Campbell", - RGB(204, 204, 204), - RGB(12, 12, 12) }; - auto& campbellTable = campbellScheme.GetTable(); - auto campbellSpan = gsl::span(&campbellTable[0], gsl::narrow(COLOR_TABLE_SIZE)); - Utils::InitializeCampbellColorTable(campbellSpan); - Utils::SetColorTableAlpha(campbellSpan, 0xff); - - return campbellScheme; -} - -// clang-format off - -ColorScheme _CreateVintageScheme() -{ - // as per https://github.com/microsoft/terminal/issues/1781 - ColorScheme vintageScheme { L"Vintage", - RGB(192, 192, 192), - RGB( 0, 0, 0) }; - auto& vintageTable = vintageScheme.GetTable(); - auto vintageSpan = gsl::span(&vintageTable[0], gsl::narrow(COLOR_TABLE_SIZE)); - vintageTable[0] = RGB( 0, 0, 0); // black - vintageTable[1] = RGB(128, 0, 0); // dark red - vintageTable[2] = RGB( 0, 128, 0); // dark green - vintageTable[3] = RGB(128, 128, 0); // dark yellow - vintageTable[4] = RGB( 0, 0, 128); // dark blue - vintageTable[5] = RGB(128, 0, 128); // dark magenta - vintageTable[6] = RGB( 0, 128, 128); // dark cyan - vintageTable[7] = RGB(192, 192, 192); // gray - vintageTable[8] = RGB(128, 128, 128); // dark gray - vintageTable[9] = RGB(255, 0, 0); // red - vintageTable[10] = RGB( 0, 255, 0); // green - vintageTable[11] = RGB(255, 255, 0); // yellow - vintageTable[12] = RGB( 0, 0, 255); // blue - vintageTable[13] = RGB(255, 0, 255); // magenta - vintageTable[14] = RGB( 0, 255, 255); // cyan - vintageTable[15] = RGB(255, 255, 255); // white - Utils::SetColorTableAlpha(vintageSpan, 0xff); - - return vintageScheme; -} - -ColorScheme _CreateOneHalfDarkScheme() -{ - // First 8 dark colors per: https://github.com/sonph/onehalf/blob/master/putty/onehalf-dark.reg - // Dark gray is per colortool scheme, the other 7 of the last 8 colors from the colortool - // scheme are the same as their dark color equivalents. - ColorScheme oneHalfDarkScheme { L"One Half Dark", - RGB(220, 223, 228), - RGB( 40, 44, 52) }; - auto& oneHalfDarkTable = oneHalfDarkScheme.GetTable(); - auto oneHalfDarkSpan = gsl::span(&oneHalfDarkTable[0], gsl::narrow(COLOR_TABLE_SIZE)); - oneHalfDarkTable[0] = RGB( 40, 44, 52); // black - oneHalfDarkTable[1] = RGB(224, 108, 117); // dark red - oneHalfDarkTable[2] = RGB(152, 195, 121); // dark green - oneHalfDarkTable[3] = RGB(229, 192, 123); // dark yellow - oneHalfDarkTable[4] = RGB( 97, 175, 239); // dark blue - oneHalfDarkTable[5] = RGB(198, 120, 221); // dark magenta - oneHalfDarkTable[6] = RGB( 86, 182, 194); // dark cyan - oneHalfDarkTable[7] = RGB(220, 223, 228); // gray - oneHalfDarkTable[8] = RGB( 90, 99, 116); // dark gray - oneHalfDarkTable[9] = RGB(224, 108, 117); // red - oneHalfDarkTable[10] = RGB(152, 195, 121); // green - oneHalfDarkTable[11] = RGB(229, 192, 123); // yellow - oneHalfDarkTable[12] = RGB( 97, 175, 239); // blue - oneHalfDarkTable[13] = RGB(198, 120, 221); // magenta - oneHalfDarkTable[14] = RGB( 86, 182, 194); // cyan - oneHalfDarkTable[15] = RGB(220, 223, 228); // white - Utils::SetColorTableAlpha(oneHalfDarkSpan, 0xff); - - return oneHalfDarkScheme; -} - -ColorScheme _CreateOneHalfLightScheme() -{ - // First 8 dark colors per: https://github.com/sonph/onehalf/blob/master/putty/onehalf-light.reg - // Last 8 colors per colortool scheme. - ColorScheme oneHalfLightScheme { L"One Half Light", - RGB(56, 58, 66), - RGB(250, 250, 250) }; - auto& oneHalfLightTable = oneHalfLightScheme.GetTable(); - auto oneHalfLightSpan = gsl::span(&oneHalfLightTable[0], gsl::narrow(COLOR_TABLE_SIZE)); - oneHalfLightTable[0] = RGB( 56, 58, 66); // black - oneHalfLightTable[1] = RGB(228, 86, 73); // dark red - oneHalfLightTable[2] = RGB( 80, 161, 79); // dark green - oneHalfLightTable[3] = RGB(193, 131, 1); // dark yellow - oneHalfLightTable[4] = RGB( 1, 132, 188); // dark blue - oneHalfLightTable[5] = RGB(166, 38, 164); // dark magenta - oneHalfLightTable[6] = RGB( 9, 151, 179); // dark cyan - oneHalfLightTable[7] = RGB(250, 250, 250); // gray - oneHalfLightTable[8] = RGB( 79, 82, 93); // dark gray - oneHalfLightTable[9] = RGB(223, 108, 117); // red - oneHalfLightTable[10] = RGB(152, 195, 121); // green - oneHalfLightTable[11] = RGB(228, 192, 122); // yellow - oneHalfLightTable[12] = RGB( 97, 175, 239); // blue - oneHalfLightTable[13] = RGB(197, 119, 221); // magenta - oneHalfLightTable[14] = RGB( 86, 181, 193); // cyan - oneHalfLightTable[15] = RGB(255, 255, 255); // white - Utils::SetColorTableAlpha(oneHalfLightSpan, 0xff); - - return oneHalfLightScheme; -} - -ColorScheme _CreateSolarizedDarkScheme() -{ - ColorScheme solarizedDarkScheme { L"Solarized Dark", - RGB(131, 148, 150), - RGB( 0, 43, 54) }; - auto& solarizedDarkTable = solarizedDarkScheme.GetTable(); - auto solarizedDarkSpan = gsl::span(&solarizedDarkTable[0], gsl::narrow(COLOR_TABLE_SIZE)); - solarizedDarkTable[0] = RGB( 7, 54, 66); - solarizedDarkTable[1] = RGB(220, 50, 47); - solarizedDarkTable[2] = RGB(133, 153, 0); - solarizedDarkTable[3] = RGB(181, 137, 0); - solarizedDarkTable[4] = RGB( 38, 139, 210); - solarizedDarkTable[5] = RGB(211, 54, 130); - solarizedDarkTable[6] = RGB( 42, 161, 152); - solarizedDarkTable[7] = RGB(238, 232, 213); - solarizedDarkTable[8] = RGB( 0, 43, 54); - solarizedDarkTable[9] = RGB(203, 75, 22); - solarizedDarkTable[10] = RGB( 88, 110, 117); - solarizedDarkTable[11] = RGB(101, 123, 131); - solarizedDarkTable[12] = RGB(131, 148, 150); - solarizedDarkTable[13] = RGB(108, 113, 196); - solarizedDarkTable[14] = RGB(147, 161, 161); - solarizedDarkTable[15] = RGB(253, 246, 227); - Utils::SetColorTableAlpha(solarizedDarkSpan, 0xff); - - return solarizedDarkScheme; -} - -ColorScheme _CreateSolarizedLightScheme() -{ - ColorScheme solarizedLightScheme { L"Solarized Light", - RGB(101, 123, 131), - RGB(253, 246, 227) }; - auto& solarizedLightTable = solarizedLightScheme.GetTable(); - auto solarizedLightSpan = gsl::span(&solarizedLightTable[0], gsl::narrow(COLOR_TABLE_SIZE)); - solarizedLightTable[0] = RGB( 7, 54, 66); - solarizedLightTable[1] = RGB(220, 50, 47); - solarizedLightTable[2] = RGB(133, 153, 0); - solarizedLightTable[3] = RGB(181, 137, 0); - solarizedLightTable[4] = RGB( 38, 139, 210); - solarizedLightTable[5] = RGB(211, 54, 130); - solarizedLightTable[6] = RGB( 42, 161, 152); - solarizedLightTable[7] = RGB(238, 232, 213); - solarizedLightTable[8] = RGB( 0, 43, 54); - solarizedLightTable[9] = RGB(203, 75, 22); - solarizedLightTable[10] = RGB( 88, 110, 117); - solarizedLightTable[11] = RGB(101, 123, 131); - solarizedLightTable[12] = RGB(131, 148, 150); - solarizedLightTable[13] = RGB(108, 113, 196); - solarizedLightTable[14] = RGB(147, 161, 161); - solarizedLightTable[15] = RGB(253, 246, 227); - Utils::SetColorTableAlpha(solarizedLightSpan, 0xff); - - return solarizedLightScheme; -} - -// clang-format on - -// Method Description: -// - Create the set of schemes to use as the default schemes. Currently creates -// five default color schemes - Campbell (the new cmd color scheme), -// One Half Dark, One Half Light, Solarized Dark, and Solarized Light. -// Arguments: -// - -// Return Value: -// - -void CascadiaSettings::_CreateDefaultSchemes() -{ - _globals.GetColorSchemes().emplace_back(_CreateCampbellScheme()); - _globals.GetColorSchemes().emplace_back(_CreateVintageScheme()); - _globals.GetColorSchemes().emplace_back(_CreateOneHalfDarkScheme()); - _globals.GetColorSchemes().emplace_back(_CreateOneHalfLightScheme()); - _globals.GetColorSchemes().emplace_back(_CreateSolarizedDarkScheme()); - _globals.GetColorSchemes().emplace_back(_CreateSolarizedLightScheme()); -} - // Method Description: // - Create a set of profiles to use as the "default" profiles when initializing // the terminal. Currently, we create two or three profiles: @@ -283,123 +103,6 @@ void CascadiaSettings::_CreateDefaultProfiles() CATCH_LOG() } -// Method Description: -// - Set up some default keybindings for the terminal. -// Arguments: -// - -// Return Value: -// - -void CascadiaSettings::_CreateDefaultKeybindings() -{ - AppKeyBindings keyBindings = _globals.GetKeybindings(); - // Set up some basic default keybindings - keyBindings.SetKeyBinding(ShortcutAction::NewTab, - KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift, - static_cast('T') }); - - keyBindings.SetKeyBinding(ShortcutAction::OpenNewTabDropdown, - KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift, - static_cast(' ') }); - - keyBindings.SetKeyBinding(ShortcutAction::DuplicateTab, - KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift, - static_cast('D') }); - - keyBindings.SetKeyBinding(ShortcutAction::ClosePane, - KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift, - static_cast('W') }); - - keyBindings.SetKeyBinding(ShortcutAction::CopyText, - KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift, - static_cast('C') }); - - keyBindings.SetKeyBinding(ShortcutAction::PasteText, - KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift, - static_cast('V') }); - - keyBindings.SetKeyBinding(ShortcutAction::OpenSettings, - KeyChord{ KeyModifiers::Ctrl, - VK_OEM_COMMA }); - - keyBindings.SetKeyBinding(ShortcutAction::NextTab, - KeyChord{ KeyModifiers::Ctrl, - VK_TAB }); - - keyBindings.SetKeyBinding(ShortcutAction::PrevTab, - KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift, - VK_TAB }); - - // Yes these are offset by one. - // Ideally, you'd want C-S-1 to open the _first_ profile, which is index 0 - keyBindings.SetKeyBinding(ShortcutAction::NewTabProfile0, - KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift, - static_cast('1') }); - keyBindings.SetKeyBinding(ShortcutAction::NewTabProfile1, - KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift, - static_cast('2') }); - keyBindings.SetKeyBinding(ShortcutAction::NewTabProfile2, - KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift, - static_cast('3') }); - keyBindings.SetKeyBinding(ShortcutAction::NewTabProfile3, - KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift, - static_cast('4') }); - keyBindings.SetKeyBinding(ShortcutAction::NewTabProfile4, - KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift, - static_cast('5') }); - keyBindings.SetKeyBinding(ShortcutAction::NewTabProfile5, - KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift, - static_cast('6') }); - keyBindings.SetKeyBinding(ShortcutAction::NewTabProfile6, - KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift, - static_cast('7') }); - keyBindings.SetKeyBinding(ShortcutAction::NewTabProfile7, - KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift, - static_cast('8') }); - keyBindings.SetKeyBinding(ShortcutAction::NewTabProfile8, - KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift, - static_cast('9') }); - - keyBindings.SetKeyBinding(ShortcutAction::ScrollUp, - KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift, - VK_UP }); - keyBindings.SetKeyBinding(ShortcutAction::ScrollDown, - KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift, - VK_DOWN }); - keyBindings.SetKeyBinding(ShortcutAction::ScrollDownPage, - KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift, - VK_NEXT }); - keyBindings.SetKeyBinding(ShortcutAction::ScrollUpPage, - KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift, - VK_PRIOR }); - keyBindings.SetKeyBinding(ShortcutAction::SwitchToTab0, - KeyChord{ KeyModifiers::Alt | KeyModifiers::Ctrl, - static_cast('1') }); - keyBindings.SetKeyBinding(ShortcutAction::SwitchToTab1, - KeyChord{ KeyModifiers::Alt | KeyModifiers::Ctrl, - static_cast('2') }); - keyBindings.SetKeyBinding(ShortcutAction::SwitchToTab2, - KeyChord{ KeyModifiers::Alt | KeyModifiers::Ctrl, - static_cast('3') }); - keyBindings.SetKeyBinding(ShortcutAction::SwitchToTab3, - KeyChord{ KeyModifiers::Alt | KeyModifiers::Ctrl, - static_cast('4') }); - keyBindings.SetKeyBinding(ShortcutAction::SwitchToTab4, - KeyChord{ KeyModifiers::Alt | KeyModifiers::Ctrl, - static_cast('5') }); - keyBindings.SetKeyBinding(ShortcutAction::SwitchToTab5, - KeyChord{ KeyModifiers::Alt | KeyModifiers::Ctrl, - static_cast('6') }); - keyBindings.SetKeyBinding(ShortcutAction::SwitchToTab6, - KeyChord{ KeyModifiers::Alt | KeyModifiers::Ctrl, - static_cast('7') }); - keyBindings.SetKeyBinding(ShortcutAction::SwitchToTab7, - KeyChord{ KeyModifiers::Alt | KeyModifiers::Ctrl, - static_cast('8') }); - keyBindings.SetKeyBinding(ShortcutAction::SwitchToTab8, - KeyChord{ KeyModifiers::Alt | KeyModifiers::Ctrl, - static_cast('9') }); -} - // Method Description: // - Initialize this object with default color schemes, profiles, and keybindings. // Arguments: @@ -409,8 +112,6 @@ void CascadiaSettings::_CreateDefaultKeybindings() void CascadiaSettings::CreateDefaults() { _CreateDefaultProfiles(); - _CreateDefaultSchemes(); - _CreateDefaultKeybindings(); } // Method Description: @@ -676,10 +377,31 @@ 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(); + + // Remove hidden profiles _after_ re-ordering. The re-ordering uses the raw + // json, and will get confused if the profile isn't in the list. + _RemoveHiddenProfiles(); + // Then do some validation on the profiles. The order of these does not // terribly matter. _ValidateNoDuplicateProfiles(); _ValidateDefaultProfileExists(); + + // TODO:GH#2547 ensure that all the profile's color scheme names are + // actually the names of schemes we've parsed. If the scheme doesn't exist, + // just use the hardcoded defaults + + // TODO:GH#2548 ensure there's at least one key bound. Display a warning if + // there's _NO_ keys bound to any actions. That's highly irregular, and + // likely an indication of an error somehow. } // Method Description: @@ -700,6 +422,18 @@ 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 valitation does not add any warnnings. +void CascadiaSettings::_ValidateProfilesHaveGuid() +{ + for (auto& profile : _profiles) + { + profile.GenerateGuidIfNecessary(); + } +} + // Method Description: // - Checks if the "globals.defaultProfile" is set to one of the profiles we // actually have. If the value is unset, or the value is set to something that @@ -742,17 +476,9 @@ void CascadiaSettings::_ValidateNoDuplicateProfiles() { bool foundDupe = false; - std::vector indiciesToDelete{}; + std::vector indiciesToDelete; - // Helper to establish an ordering on guids - struct GuidEquality - { - bool operator()(const GUID& lhs, const GUID& rhs) const - { - return memcmp(&lhs, &rhs, sizeof(rhs)) < 0; - } - }; - std::set uniqueGuids{}; + std::set uniqueGuids; // Try collecting all the unique guids. If we ever encounter a guid that's // already in the set, then we need to delete that profile. @@ -777,3 +503,77 @@ void CascadiaSettings::_ValidateNoDuplicateProfiles() _warnings.push_back(::TerminalApp::SettingsLoadWarnings::DuplicateProfile); } } + +// Method Description: +// - Re-orders the list of profiles to match what the user would expect them to +// be. Orders profiles to be in the ordering { [profiles from user settings], +// [default profiles that weren't in the user profiles]}. +// - Does not set any warnings. +// Arguments: +// - +// Return Value: +// - +void CascadiaSettings::_ReorderProfilesToMatchUserSettingsOrder() +{ + std::set uniqueGuids; + std::deque guidOrder; + + auto collectGuids = [&](const auto& json) { + for (auto profileJson : _GetProfilesJsonObject(json)) + { + if (profileJson.isObject()) + { + auto guid = Profile::GetGuidOrGenerateForJson(profileJson); + if (uniqueGuids.insert(guid).second) + { + guidOrder.push_back(guid); + } + } + } + }; + + // Push all the userSettings profiles' GUIDS into the set + collectGuids(_userSettings); + + // Push all the defaultSettings profiles' GUIDS into the set + collectGuids(_defaultSettings); + std::equal_to equals; + // Re-order the list of _profiles to match that ordering + // for (gIndex=0 -> uniqueGuids.size) + // pIndex = the pIndex of the profile with guid==guids[gIndex] + // profiles.swap(pIndex <-> gIndex) + // This is O(N^2), which is kinda rough. I'm sure there's a better way + for (size_t gIndex = 0; gIndex < guidOrder.size(); gIndex++) + { + const auto guid = guidOrder.at(gIndex); + for (size_t pIndex = gIndex; pIndex < _profiles.size(); pIndex++) + { + auto profileGuid = _profiles.at(pIndex).GetGuid(); + if (equals(profileGuid, guid)) + { + std::iter_swap(_profiles.begin() + pIndex, _profiles.begin() + gIndex); + break; + } + } + } +} + +// Method Description: +// - Removes any profiles marked "hidden" from the list of profiles. +// - Does not set any warnings. +// Arguments: +// - +// Return Value: +// - +void CascadiaSettings::_RemoveHiddenProfiles() +{ + // remove_if will move all the profiles where the lambda is true to the end + // of the list, then return a iterator to the point in the list where those + // profiles start. The erase call will then remove all of those profiles + // from the list. This is the [erase-remove + // idiom](https://en.wikipedia.org/wiki/Erase%E2%80%93remove_idiom) + _profiles.erase(std::remove_if(_profiles.begin(), + _profiles.end(), + [](auto&& profile) { return profile.IsHidden(); }), + _profiles.end()); +} diff --git a/src/cascadia/TerminalApp/CascadiaSettings.h b/src/cascadia/TerminalApp/CascadiaSettings.h index 703cdaa0990..746e9563388 100644 --- a/src/cascadia/TerminalApp/CascadiaSettings.h +++ b/src/cascadia/TerminalApp/CascadiaSettings.h @@ -16,7 +16,7 @@ Author(s): --*/ #pragma once -#include +#include #include "GlobalAppSettings.h" #include "TerminalWarnings.h" #include "Profile.h" @@ -27,6 +27,9 @@ static constexpr GUID AzureConnectionType = { 0xd9fcfdfa, 0xa479, 0x412c, { 0x83 namespace TerminalAppLocalTests { class SettingsTests; + class ProfileTests; + class ColorSchemeTests; + class KeyBindingsTests; } namespace TerminalApp @@ -40,6 +43,7 @@ class TerminalApp::CascadiaSettings final CascadiaSettings(); ~CascadiaSettings(); + static std::unique_ptr LoadDefaults(); static std::unique_ptr LoadAll(); void SaveAll() const; @@ -53,8 +57,10 @@ class TerminalApp::CascadiaSettings final Json::Value ToJson() const; static std::unique_ptr FromJson(const Json::Value& json); + void LayerJson(const Json::Value& json); static std::wstring GetSettingsPath(const bool useRoamingPath = false); + static std::wstring GetDefaultSettingsPath(); const Profile* FindProfile(GUID profileGuid) const noexcept; @@ -65,20 +71,32 @@ class TerminalApp::CascadiaSettings final private: GlobalAppSettings _globals; std::vector _profiles; - std::vector _warnings{}; + std::vector _warnings; + + Json::Value _userSettings; + Json::Value _defaultSettings; - void _CreateDefaultKeybindings(); - void _CreateDefaultSchemes(); void _CreateDefaultProfiles(); + void _LayerOrCreateProfile(const Json::Value& profileJson); + Profile* _FindMatchingProfile(const Json::Value& profileJson); + void _LayerOrCreateColorScheme(const Json::Value& schemeJson); + ColorScheme* _FindMatchingColorScheme(const Json::Value& schemeJson); + void _LayerJsonString(std::string_view fileData, const bool isDefaultSettings); + static const Json::Value& _GetProfilesJsonObject(const Json::Value& json); + static bool _IsPackaged(); static void _WriteSettings(const std::string_view content); - static std::optional _ReadSettings(); + static std::optional _ReadUserSettings(); + static std::optional _ReadFile(HANDLE hFile); void _ValidateSettings(); void _ValidateProfilesExist(); + void _ValidateProfilesHaveGuid(); void _ValidateDefaultProfileExists(); void _ValidateNoDuplicateProfiles(); + void _ReorderProfilesToMatchUserSettingsOrder(); + void _RemoveHiddenProfiles(); static bool _isPowerShellCoreInstalledInPath(const std::wstring_view programFileEnv, std::filesystem::path& cmdline); static bool _isPowerShellCoreInstalled(std::filesystem::path& cmdline); @@ -86,4 +104,7 @@ class TerminalApp::CascadiaSettings final static Profile _CreateDefaultProfile(const std::wstring_view name); friend class TerminalAppLocalTests::SettingsTests; + friend class TerminalAppLocalTests::ProfileTests; + friend class TerminalAppLocalTests::ColorSchemeTests; + friend class TerminalAppLocalTests::KeyBindingsTests; }; diff --git a/src/cascadia/TerminalApp/CascadiaSettingsSerialization.cpp b/src/cascadia/TerminalApp/CascadiaSettingsSerialization.cpp index d8b35a0a675..3bf4c31dcb9 100644 --- a/src/cascadia/TerminalApp/CascadiaSettingsSerialization.cpp +++ b/src/cascadia/TerminalApp/CascadiaSettingsSerialization.cpp @@ -4,11 +4,19 @@ #include "pch.h" #include #include "CascadiaSettings.h" -#include "AppKeyBindingsSerialization.h" #include "../../types/inc/utils.hpp" +#include "utils.h" +#include "JsonUtils.h" #include #include +// defaults.h is a file containing the default json settings in a std::string_view +#include "defaults.h" +// userDefault.h is like the above, but with a default template for the user's profiles.json. +#include "userDefaults.h" +// Both defaults.h and userDefaults.h are generated at build time into the +// "Generated Files" directory. + using namespace ::TerminalApp; using namespace winrt::Microsoft::Terminal::TerminalControl; using namespace winrt::TerminalApp; @@ -17,6 +25,8 @@ using namespace ::Microsoft::Console; static constexpr std::wstring_view SettingsFilename{ L"profiles.json" }; static constexpr std::wstring_view UnpackagedSettingsFolderName{ L"Microsoft\\Windows Terminal\\" }; +static constexpr std::wstring_view DefaultsFilename{ L"defaults.json" }; + static constexpr std::string_view ProfilesKey{ "profiles" }; static constexpr std::string_view KeybindingsKey{ "keybindings" }; static constexpr std::string_view GlobalsKey{ "globals" }; @@ -30,54 +40,93 @@ static constexpr std::string_view Utf8Bom{ u8"\uFEFF" }; // 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 profiles.json // Return Value: // - a unique_ptr containing a new CascadiaSettings object. std::unique_ptr CascadiaSettings::LoadAll() { - std::unique_ptr resultPtr; - std::optional fileData = _ReadSettings(); + auto resultPtr = LoadDefaults(); + std::optional fileData = _ReadUserSettings(); const bool foundFile = fileData.has_value(); // Make sure the file isn't totally empty. If it is, we'll treat the file // like it doesn't exist at all. const bool fileHasData = foundFile && !fileData.value().empty(); - if (foundFile && fileHasData) + if (fileHasData) { - const auto actualData = fileData.value(); + resultPtr->_LayerJsonString(fileData.value(), false); + } + else + { + // We didn't find the user settings. We'll need to create a file + // to use as the user defaults. + _WriteSettings(UserSettingsJson); + } - // Ignore UTF-8 BOM - auto actualDataStart = actualData.c_str(); - if (actualData.compare(0, Utf8Bom.size(), Utf8Bom) == 0) - { - actualDataStart += Utf8Bom.size(); - } + // If this throws, the app will catch it and use the default settings + resultPtr->_ValidateSettings(); - // Parse the json data. - Json::Value root; - std::unique_ptr reader{ Json::CharReaderBuilder::CharReaderBuilder().newCharReader() }; - std::string errs; // This string will recieve any error text from failing to parse. - // `parse` will return false if it fails. - if (!reader->parse(actualDataStart, actualData.c_str() + actualData.size(), &root, &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)); - } - resultPtr = FromJson(root); + return resultPtr; +} - // If this throws, the app will catch it and use the default settings (temporarily) - resultPtr->_ValidateSettings(); - } - else +// Function Description: +// - Creates a new CascadiaSettings object initialized with settings from the +// hardcoded defaults.json. +// Arguments: +// - +// Return Value: +// - a unique_ptr to a CascadiaSettings with the settings from defaults.json +std::unique_ptr CascadiaSettings::LoadDefaults() +{ + auto resultPtr = std::make_unique(); + + // We already have the defaults in memory, because we stamp them into a + // header as part of the build process. We don't need to bother with reading + // them from a file (and the potential that could fail) + resultPtr->_LayerJsonString(DefaultJson, true); + + return resultPtr; +} + +// Method Description: +// - Attempts to read the given data as a string of JSON, parse that JSON, and +// then layer the settings from that JSON object on our current settings. See +// CascadiaSettings::LayerJson for detauls on how the layering works. +// - Will ignore leading UTF-8 BOMs. +// - Additionally, will store the parsed JSON in this object, as either our +// _defaultSettings or our _userSettings, depending on isDefaultSettings. +// Arguments: +// - fileData: the string to parse as JSON data +// - isDefaultSettings: if true, we should store the parsed JSON as our +// defaultSettings. Otherwise, we'll store the parsed JSON as our user +// settings. +// Return Value: +// - +void CascadiaSettings::_LayerJsonString(std::string_view fileData, const bool isDefaultSettings) +{ + // Ignore UTF-8 BOM + auto actualDataStart = fileData.data(); + if (fileData.compare(0, Utf8Bom.size(), Utf8Bom) == 0) { - resultPtr = std::make_unique(); - resultPtr->CreateDefaults(); + actualDataStart += Utf8Bom.size(); + } + + std::string errs; // This string will recieve any error text from failing to parse. + std::unique_ptr reader{ Json::CharReaderBuilder::CharReaderBuilder().newCharReader() }; - // The settings file does not exist. Let's commit one. - resultPtr->SaveAll(); + // 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, fileData.data() + fileData.size(), &root, &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)); } - return resultPtr; + LayerJson(root); } // Method Description: @@ -137,13 +186,28 @@ Json::Value CascadiaSettings::ToJson() const // - a new CascadiaSettings instance created from the values in `json` std::unique_ptr CascadiaSettings::FromJson(const Json::Value& json) { - std::unique_ptr resultPtr = std::make_unique(); + auto resultPtr = std::make_unique(); + resultPtr->LayerJson(json); + return resultPtr; +} +// Method Description: +// - Layer values from the given json object on top of the existing properties +// of this object. For any keys we're expecting to be able to parse in the +// given object, we'll parse them and replace our settings with values from +// the new json object. Properties that _aren't_ in the json object will _not_ +// be replaced. +// Arguments: +// - json: an object which should be a partial serialization of a CascadiaSettings object. +// Return Value: +// +void CascadiaSettings::LayerJson(const Json::Value& json) +{ if (auto globals{ json[GlobalsKey.data()] }) { if (globals.isObject()) { - resultPtr->_globals = GlobalAppSettings::FromJson(globals); + _globals.LayerJson(globals); } } else @@ -151,53 +215,127 @@ std::unique_ptr CascadiaSettings::FromJson(const Json::Value& // If there's no globals key in the root object, then try looking at the // root object for those properties instead, to gracefully upgrade. // This will attempt to do the legacy keybindings loading too - resultPtr->_globals = GlobalAppSettings::FromJson(json); - - // If we didn't find keybindings in the legacy path, then they probably - // don't exist in the file. Create the default keybindings if we - // couldn't find any keybindings. - auto keybindings{ json[KeybindingsKey.data()] }; - if (!keybindings) - { - resultPtr->_CreateDefaultKeybindings(); - } + _globals.LayerJson(json); } - // TODO:MSFT:20737698 - Display an error if we failed to parse settings - // What should we do here if these keys aren't found?For default profile, - // we could always pick the first profile and just set that as the default. - // Finding no schemes is probably fine, unless of course one profile - // references a scheme. We could fail with come error saying the - // profiles file is corrupted. - // Not having any profiles is also bad - should we say the file is corrupted? - // Or should we just recreate the default profiles? - - auto& resultSchemes = resultPtr->_globals.GetColorSchemes(); if (auto schemes{ json[SchemesKey.data()] }) { for (auto schemeJson : schemes) { if (schemeJson.isObject()) { - auto scheme = ColorScheme::FromJson(schemeJson); - resultSchemes.emplace_back(std::move(scheme)); + _LayerOrCreateColorScheme(schemeJson); } } } - if (auto profiles{ json[ProfilesKey.data()] }) + for (auto profileJson : _GetProfilesJsonObject(json)) { - for (auto profileJson : profiles) + if (profileJson.isObject()) { - if (profileJson.isObject()) - { - auto profile = Profile::FromJson(profileJson); - resultPtr->_profiles.emplace_back(profile); - } + _LayerOrCreateProfile(profileJson); } } +} - return resultPtr; +// Method Description: +// - Given a partial json serialization of a Profile object, either layers that +// json on a matching Profile we already have, or creates a new Profile +// object from those settings. +// Arguments: +// - json: an object which may be a partial serialization of a Profile object. +// Return Value: +// - +void CascadiaSettings::_LayerOrCreateProfile(const Json::Value& profileJson) +{ + // Layer the json on top of an existing profile, if we have one: + auto pProfile = _FindMatchingProfile(profileJson); + if (pProfile) + { + pProfile->LayerJson(profileJson); + } + else + { + auto profile = Profile::FromJson(profileJson); + _profiles.emplace_back(profile); + } +} + +// Method Description: +// - Finds a profile from our list of profiles that matches the given json +// object. Uses Profile::ShouldBeLayered to determine if the Json::Value is a +// match or not. This method should be used to find a profile to layer the +// given settings upon. +// - Returns nullptr if no such match exists. +// Arguments: +// - json: an object which may be a partial serialization of a Profile object. +// Return Value: +// - a Profile that can be layered with the given json object, iff such a +// profile exists. +Profile* CascadiaSettings::_FindMatchingProfile(const Json::Value& profileJson) +{ + for (auto& profile : _profiles) + { + if (profile.ShouldBeLayered(profileJson)) + { + // HERE BE DRAGONS: Returning a pointer to a type in the vector is + // maybe not the _safest_ thing, but we have a mind to make Profile + // and ColorScheme winrt types in the future, so this will be safer + // then. + return &profile; + } + } + return nullptr; +} + +// Method Description: +// - Given a partial json serialization of a ColorScheme object, either layers that +// json on a matching ColorScheme we already have, or creates a new ColorScheme +// object from those settings. +// Arguments: +// - json: an object which should be a partial serialization of a ColorScheme object. +// Return Value: +// - +void CascadiaSettings::_LayerOrCreateColorScheme(const Json::Value& schemeJson) +{ + // Layer the json on top of an existing profile, if we have one: + auto pScheme = _FindMatchingColorScheme(schemeJson); + if (pScheme) + { + pScheme->LayerJson(schemeJson); + } + else + { + auto scheme = ColorScheme::FromJson(schemeJson); + _globals.GetColorSchemes().emplace_back(scheme); + } +} + +// Method Description: +// - Finds a color scheme from our list of color schemes that matches the given +// json object. Uses ColorScheme::ShouldBeLayered to determine if the +// Json::Value is a match or not. This method should be used to find a color +// scheme to layer the given settings upon. +// - Returns nullptr if no such match exists. +// Arguments: +// - json: an object which should be a partial serialization of a ColorScheme object. +// Return Value: +// - a ColorScheme that can be layered with the given json object, iff such a +// color scheme exists. +ColorScheme* CascadiaSettings::_FindMatchingColorScheme(const Json::Value& schemeJson) +{ + for (auto& scheme : _globals.GetColorSchemes()) + { + if (scheme.ShouldBeLayered(schemeJson)) + { + // HERE BE DRAGONS: Returning a pointer to a type in the vector is + // maybe not the _safest_ thing, but we have a mind to make Profile + // and ColorScheme winrt types in the future, so this will be safer + // then. + return &scheme; + } + } + return nullptr; } // Function Description: @@ -227,13 +365,18 @@ void CascadiaSettings::_WriteSettings(const std::string_view content) { auto pathToSettingsFile{ CascadiaSettings::GetSettingsPath() }; - auto hOut = CreateFileW(pathToSettingsFile.c_str(), GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); - if (hOut == INVALID_HANDLE_VALUE) + wil::unique_hfile hOut{ CreateFileW(pathToSettingsFile.c_str(), + GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, + NULL, + CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, + NULL) }; + if (!hOut) { THROW_LAST_ERROR(); } - THROW_LAST_ERROR_IF(!WriteFile(hOut, content.data(), gsl::narrow(content.size()), 0, 0)); - CloseHandle(hOut); + THROW_LAST_ERROR_IF(!WriteFile(hOut.get(), content.data(), gsl::narrow(content.size()), 0, 0)); } // Method Description: @@ -245,7 +388,7 @@ void CascadiaSettings::_WriteSettings(const std::string_view content) // otherwise the optional will be empty. // If the file exists, but we fail to read it, this can throw an exception // from reading the file -std::optional CascadiaSettings::_ReadSettings() +std::optional CascadiaSettings::_ReadUserSettings() { const auto pathToSettingsFile{ CascadiaSettings::GetSettingsPath() }; wil::unique_hfile hFile{ CreateFileW(pathToSettingsFile.c_str(), @@ -310,14 +453,26 @@ std::optional CascadiaSettings::_ReadSettings() } } + return _ReadFile(hFile.get()); +} + +// Method Description: +// - Reads the content in UTF-8 encoding of the given file using the Win32 APIs +// Arguments: +// - +// Return Value: +// - an optional with the content of the file if we were able to read it. If we +// fail to read it, this can throw an exception from reading the file +std::optional CascadiaSettings::_ReadFile(HANDLE hFile) +{ // fileSize is in bytes - const auto fileSize = GetFileSize(hFile.get(), nullptr); + const auto fileSize = GetFileSize(hFile, nullptr); THROW_LAST_ERROR_IF(fileSize == INVALID_FILE_SIZE); auto utf8buffer = std::make_unique(fileSize); DWORD bytesRead = 0; - THROW_LAST_ERROR_IF(!ReadFile(hFile.get(), utf8buffer.get(), fileSize, &bytesRead, nullptr)); + THROW_LAST_ERROR_IF(!ReadFile(hFile, utf8buffer.get(), fileSize, &bytesRead, nullptr)); // convert buffer to UTF-8 string std::string utf8string(utf8buffer.get(), fileSize); @@ -359,3 +514,38 @@ std::wstring CascadiaSettings::GetSettingsPath(const bool useRoamingPath) return parentDirectoryForSettingsFile / SettingsFilename; } + +std::wstring CascadiaSettings::GetDefaultSettingsPath() +{ + // 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. + + HMODULE hModule = GetModuleHandle(nullptr); + THROW_LAST_ERROR_IF(hModule == nullptr); + + std::wstring exePathString; + THROW_IF_FAILED(wil::GetModuleFileNameW(hModule, exePathString)); + + const std::filesystem::path exePath{ exePathString }; + const std::filesystem::path rootDir = exePath.parent_path(); + return rootDir / DefaultsFilename; +} + +// Function Description: +// - Gets the object in the given JSON object under the "profiles" key. Returns +// null if there's no "profiles" key. +// Arguments: +// - json: the json object to get the profiles from. +// Return Value: +// - the Json::Value representing the profiles property from the given object +const Json::Value& CascadiaSettings::_GetProfilesJsonObject(const Json::Value& json) +{ + return json[JsonKey(ProfilesKey)]; +} diff --git a/src/cascadia/TerminalApp/ColorScheme.cpp b/src/cascadia/TerminalApp/ColorScheme.cpp index 4fe57f7b00c..6ced95b2426 100644 --- a/src/cascadia/TerminalApp/ColorScheme.cpp +++ b/src/cascadia/TerminalApp/ColorScheme.cpp @@ -5,9 +5,10 @@ #include "ColorScheme.h" #include "../../types/inc/Utils.hpp" #include "Utils.h" +#include "JsonUtils.h" -using namespace TerminalApp; using namespace ::Microsoft::Console; +using namespace TerminalApp; using namespace winrt::Microsoft::Terminal::Settings; using namespace winrt::Microsoft::Terminal::TerminalControl; @@ -105,21 +106,54 @@ Json::Value ColorScheme::ToJson() const // - a new ColorScheme instance created from the values in `json` ColorScheme ColorScheme::FromJson(const Json::Value& json) { - ColorScheme result{}; + ColorScheme result; + result.LayerJson(json); + return result; +} + +// Method Description: +// - Returns true if we think the provided json object represents an instance of +// the same object as this object. If true, we should layer that json object +// on us, instead of creating a new object. +// Arguments: +// - json: The json object to query to see if it's the same +// Return Value: +// - true iff the json object has the same `name` as we do. +bool ColorScheme::ShouldBeLayered(const Json::Value& json) const +{ + if (const auto name{ json[JsonKey(NameKey)] }) + { + const auto nameFromJson = GetWstringFromJson(name); + return nameFromJson == _schemeName; + } + return false; +} +// Method Description: +// - Layer values from the given json object on top of the existing properties +// of this object. For any keys we're expecting to be able to parse in the +// given object, we'll parse them and replace our settings with values from +// the new json object. Properties that _aren't_ in the json object will _not_ +// be replaced. +// Arguments: +// - json: an object which should be a partial serialization of a ColorScheme object. +// Return Value: +// +void ColorScheme::LayerJson(const Json::Value& json) +{ if (auto name{ json[JsonKey(NameKey)] }) { - result._schemeName = winrt::to_hstring(name.asString()); + _schemeName = winrt::to_hstring(name.asString()); } if (auto fgString{ json[JsonKey(ForegroundKey)] }) { const auto color = Utils::ColorFromHexString(fgString.asString()); - result._defaultForeground = color; + _defaultForeground = color; } if (auto bgString{ json[JsonKey(BackgroundKey)] }) { const auto color = Utils::ColorFromHexString(bgString.asString()); - result._defaultBackground = color; + _defaultBackground = color; } // Legacy Deserialization. Leave in place to allow forward compatibility @@ -132,7 +166,7 @@ ColorScheme ColorScheme::FromJson(const Json::Value& json) if (tableEntry.isString()) { auto color = Utils::ColorFromHexString(tableEntry.asString()); - result._table.at(i) = color; + _table.at(i) = color; } i++; } @@ -144,12 +178,10 @@ ColorScheme ColorScheme::FromJson(const Json::Value& json) if (auto str{ json[JsonKey(current)] }) { const auto color = Utils::ColorFromHexString(str.asString()); - result._table.at(i) = color; + _table.at(i) = color; } i++; } - - return result; } std::wstring_view ColorScheme::GetName() const noexcept diff --git a/src/cascadia/TerminalApp/ColorScheme.h b/src/cascadia/TerminalApp/ColorScheme.h index fd3595a20cd..4ce1831cef0 100644 --- a/src/cascadia/TerminalApp/ColorScheme.h +++ b/src/cascadia/TerminalApp/ColorScheme.h @@ -19,6 +19,13 @@ Author(s): #include #include "../../inc/conattrs.hpp" +// fwdecl unittest classes +namespace TerminalAppLocalTests +{ + class SettingsTests; + class ColorSchemeTests; +}; + namespace TerminalApp { class ColorScheme; @@ -35,6 +42,8 @@ class TerminalApp::ColorScheme Json::Value ToJson() const; static ColorScheme FromJson(const Json::Value& json); + bool ShouldBeLayered(const Json::Value& json) const; + void LayerJson(const Json::Value& json); std::wstring_view GetName() const noexcept; std::array& GetTable() noexcept; @@ -46,4 +55,7 @@ class TerminalApp::ColorScheme std::array _table; COLORREF _defaultForeground; COLORREF _defaultBackground; + + friend class TerminalAppLocalTests::SettingsTests; + friend class TerminalAppLocalTests::ColorSchemeTests; }; diff --git a/src/cascadia/TerminalApp/GlobalAppSettings.cpp b/src/cascadia/TerminalApp/GlobalAppSettings.cpp index eba5beedca7..2fe54b0540c 100644 --- a/src/cascadia/TerminalApp/GlobalAppSettings.cpp +++ b/src/cascadia/TerminalApp/GlobalAppSettings.cpp @@ -5,8 +5,8 @@ #include "GlobalAppSettings.h" #include "../../types/inc/Utils.hpp" #include "../../inc/DefaultSettings.h" -#include "AppKeyBindingsSerialization.h" #include "Utils.h" +#include "JsonUtils.h" using namespace TerminalApp; using namespace winrt::Microsoft::Terminal::Settings; @@ -31,7 +31,7 @@ static constexpr std::wstring_view DarkThemeValue{ L"dark" }; static constexpr std::wstring_view SystemThemeValue{ L"system" }; GlobalAppSettings::GlobalAppSettings() : - _keybindings{}, + _keybindings{ winrt::make_self() }, _colorSchemes{}, _defaultProfile{}, _alwaysShowTabs{ true }, @@ -71,12 +71,7 @@ GUID GlobalAppSettings::GetDefaultProfile() const noexcept AppKeyBindings GlobalAppSettings::GetKeybindings() const noexcept { - return _keybindings; -} - -void GlobalAppSettings::SetKeybindings(winrt::TerminalApp::AppKeyBindings newBindings) noexcept -{ - _keybindings = newBindings; + return *_keybindings; } bool GlobalAppSettings::GetAlwaysShowTabs() const noexcept @@ -175,7 +170,7 @@ Json::Value GlobalAppSettings::ToJson() const jsonObject[JsonKey(WordDelimitersKey)] = winrt::to_string(_wordDelimiters); jsonObject[JsonKey(CopyOnSelectKey)] = _copyOnSelect; jsonObject[JsonKey(RequestedThemeKey)] = winrt::to_string(_SerializeTheme(_requestedTheme)); - jsonObject[JsonKey(KeybindingsKey)] = AppKeyBindingsSerialization::ToJson(_keybindings); + jsonObject[JsonKey(KeybindingsKey)] = _keybindings->ToJson(); return jsonObject; } @@ -188,58 +183,61 @@ Json::Value GlobalAppSettings::ToJson() const // - a new GlobalAppSettings instance created from the values in `json` GlobalAppSettings GlobalAppSettings::FromJson(const Json::Value& json) { - GlobalAppSettings result{}; + GlobalAppSettings result; + result.LayerJson(json); + return result; +} +void GlobalAppSettings::LayerJson(const Json::Value& json) +{ if (auto defaultProfile{ json[JsonKey(DefaultProfileKey)] }) { auto guid = Utils::GuidFromString(GetWstringFromJson(defaultProfile)); - result._defaultProfile = guid; + _defaultProfile = guid; } if (auto alwaysShowTabs{ json[JsonKey(AlwaysShowTabsKey)] }) { - result._alwaysShowTabs = alwaysShowTabs.asBool(); + _alwaysShowTabs = alwaysShowTabs.asBool(); } if (auto initialRows{ json[JsonKey(InitialRowsKey)] }) { - result._initialRows = initialRows.asInt(); + _initialRows = initialRows.asInt(); } if (auto initialCols{ json[JsonKey(InitialColsKey)] }) { - result._initialCols = initialCols.asInt(); + _initialCols = initialCols.asInt(); } if (auto showTitleInTitlebar{ json[JsonKey(ShowTitleInTitlebarKey)] }) { - result._showTitleInTitlebar = showTitleInTitlebar.asBool(); + _showTitleInTitlebar = showTitleInTitlebar.asBool(); } if (auto showTabsInTitlebar{ json[JsonKey(ShowTabsInTitlebarKey)] }) { - result._showTabsInTitlebar = showTabsInTitlebar.asBool(); + _showTabsInTitlebar = showTabsInTitlebar.asBool(); } if (auto wordDelimiters{ json[JsonKey(WordDelimitersKey)] }) { - result._wordDelimiters = GetWstringFromJson(wordDelimiters); + _wordDelimiters = GetWstringFromJson(wordDelimiters); } if (auto copyOnSelect{ json[JsonKey(CopyOnSelectKey)] }) { - result._copyOnSelect = copyOnSelect.asBool(); + _copyOnSelect = copyOnSelect.asBool(); } if (auto requestedTheme{ json[JsonKey(RequestedThemeKey)] }) { - result._requestedTheme = _ParseTheme(GetWstringFromJson(requestedTheme)); + _requestedTheme = _ParseTheme(GetWstringFromJson(requestedTheme)); } if (auto keybindings{ json[JsonKey(KeybindingsKey)] }) { - result._keybindings = AppKeyBindingsSerialization::FromJson(keybindings); + _keybindings->LayerJson(keybindings); } - - return result; } // Method Description: diff --git a/src/cascadia/TerminalApp/GlobalAppSettings.h b/src/cascadia/TerminalApp/GlobalAppSettings.h index 11dab24ccaa..b70640019f3 100644 --- a/src/cascadia/TerminalApp/GlobalAppSettings.h +++ b/src/cascadia/TerminalApp/GlobalAppSettings.h @@ -17,6 +17,12 @@ Author(s): #include "AppKeyBindings.h" #include "ColorScheme.h" +// fwdecl unittest classes +namespace TerminalAppLocalTests +{ + class SettingsTests; +}; + namespace TerminalApp { class GlobalAppSettings; @@ -34,7 +40,6 @@ class TerminalApp::GlobalAppSettings final GUID GetDefaultProfile() const noexcept; winrt::TerminalApp::AppKeyBindings GetKeybindings() const noexcept; - void SetKeybindings(winrt::TerminalApp::AppKeyBindings newBindings) noexcept; bool GetAlwaysShowTabs() const noexcept; void SetAlwaysShowTabs(const bool showTabs) noexcept; @@ -57,12 +62,13 @@ class TerminalApp::GlobalAppSettings final Json::Value ToJson() const; static GlobalAppSettings FromJson(const Json::Value& json); + void LayerJson(const Json::Value& json); void ApplyToSettings(winrt::Microsoft::Terminal::Settings::TerminalSettings& settings) const noexcept; private: GUID _defaultProfile; - winrt::TerminalApp::AppKeyBindings _keybindings; + winrt::com_ptr _keybindings; std::vector _colorSchemes; @@ -80,4 +86,6 @@ class TerminalApp::GlobalAppSettings final static winrt::Windows::UI::Xaml::ElementTheme _ParseTheme(const std::wstring& themeString) noexcept; static std::wstring_view _SerializeTheme(const winrt::Windows::UI::Xaml::ElementTheme theme) noexcept; + + friend class TerminalAppLocalTests::SettingsTests; }; diff --git a/src/cascadia/TerminalApp/JsonUtils.cpp b/src/cascadia/TerminalApp/JsonUtils.cpp new file mode 100644 index 00000000000..09363d0ad98 --- /dev/null +++ b/src/cascadia/TerminalApp/JsonUtils.cpp @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "Utils.h" +#include "JsonUtils.h" +#include "../../types/inc/Utils.hpp" + +void TerminalApp::JsonUtils::GetOptionalColor(const Json::Value& json, + std::string_view key, + std::optional& target) +{ + const auto conversionFn = [](const Json::Value& value) -> uint32_t { + return ::Microsoft::Console::Utils::ColorFromHexString(value.asString()); + }; + GetOptionalValue(json, + key, + target, + conversionFn); +} + +void TerminalApp::JsonUtils::GetOptionalString(const Json::Value& json, + std::string_view key, + std::optional& target) +{ + const auto conversionFn = [](const Json::Value& value) -> std::wstring { + return GetWstringFromJson(value); + }; + GetOptionalValue(json, + key, + target, + conversionFn); +} + +void TerminalApp::JsonUtils::GetOptionalGuid(const Json::Value& json, + std::string_view key, + std::optional& target) +{ + const auto conversionFn = [](const Json::Value& value) -> GUID { + return ::Microsoft::Console::Utils::GuidFromString(GetWstringFromJson(value)); + }; + GetOptionalValue(json, + key, + target, + conversionFn); +} + +void TerminalApp::JsonUtils::GetOptionalDouble(const Json::Value& json, + std::string_view key, + std::optional& target) +{ + const auto conversionFn = [](const Json::Value& value) -> double { + return value.asFloat(); + }; + GetOptionalValue(json, + key, + target, + conversionFn); +} diff --git a/src/cascadia/TerminalApp/JsonUtils.h b/src/cascadia/TerminalApp/JsonUtils.h new file mode 100644 index 00000000000..e4a0865906f --- /dev/null +++ b/src/cascadia/TerminalApp/JsonUtils.h @@ -0,0 +1,69 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- JsonUtils.h + +Abstract: +- Helpers for the TerminalApp project +Author(s): +- Mike Griese - August 2019 + +--*/ +#pragma once + +namespace TerminalApp::JsonUtils +{ + void GetOptionalColor(const Json::Value& json, + std::string_view key, + std::optional& target); + + void GetOptionalString(const Json::Value& json, + std::string_view key, + std::optional& target); + + void GetOptionalGuid(const Json::Value& json, + std::string_view key, + std::optional& target); + + void GetOptionalDouble(const Json::Value& json, + std::string_view key, + std::optional& target); + + // Method Description: + // - Helper that can be used for retrieving an optional value from a json + // object, and parsing it's value to layer on a given target object. + // - If the key we're looking for _doesn't_ exist in the json object, + // we'll leave the target object unmodified. + // - If the key exists in the json object, but is set to `null`, then + // we'll instead set the target back to nullopt. + // - Each caller should provide a conversion function that takes a + // Json::Value and returns an object of the same type as target. + // Arguments: + // - json: The json object to search for the given key + // - key: The key to look for in the json object + // - target: the optional object to recieve the value from json + // - conversion: a std::function which can be used to + // convert the Json::Value to the appropriate type. + // Return Value: + // - + template + void GetOptionalValue(const Json::Value& json, + std::string_view key, + std::optional& target, + F&& conversion) + { + if (json.isMember(JsonKey(key))) + { + if (auto jsonVal{ json[JsonKey(key)] }) + { + target = conversion(jsonVal); + } + else + { + target = std::nullopt; + } + } + } +}; diff --git a/src/cascadia/TerminalApp/Profile.cpp b/src/cascadia/TerminalApp/Profile.cpp index 5863c3aae38..2112612ae65 100644 --- a/src/cascadia/TerminalApp/Profile.cpp +++ b/src/cascadia/TerminalApp/Profile.cpp @@ -4,6 +4,7 @@ #include "pch.h" #include "Profile.h" #include "Utils.h" +#include "JsonUtils.h" #include "../../types/inc/Utils.hpp" #include @@ -15,6 +16,7 @@ static constexpr std::string_view NameKey{ "name" }; static constexpr std::string_view GuidKey{ "guid" }; static constexpr std::string_view ColorSchemeKey{ "colorScheme" }; static constexpr std::string_view ColorSchemeKeyOld{ "colorscheme" }; +static constexpr std::string_view HiddenKey{ "hidden" }; static constexpr std::string_view ForegroundKey{ "foreground" }; static constexpr std::string_view BackgroundKey{ "background" }; @@ -71,14 +73,15 @@ static constexpr std::string_view ImageAlignmentBottomLeft{ "bottomLeft" }; static constexpr std::string_view ImageAlignmentBottomRight{ "bottomRight" }; Profile::Profile() : - Profile(Utils::CreateGuid()) + Profile(std::nullopt) { } -Profile::Profile(const winrt::guid& guid) : +Profile::Profile(const std::optional& guid) : _guid(guid), _name{ L"Default" }, - _schemeName{}, + _schemeName{ L"Campbell" }, + _hidden{ false }, _defaultForeground{}, _defaultBackground{}, @@ -114,7 +117,9 @@ Profile::~Profile() GUID Profile::GetGuid() const noexcept { - return _guid; + // This can throw if we never had our guid set to a legitimate value. + THROW_HR_IF_MSG(E_FAIL, !_guid.has_value(), "Profile._guid always expected to have a value"); + return _guid.value(); } // Function Description: @@ -242,8 +247,12 @@ Json::Value Profile::ToJson() const Json::Value root; ///// Profile-specific settings ///// - root[JsonKey(GuidKey)] = winrt::to_string(Utils::GuidToString(_guid)); + if (_guid.has_value()) + { + root[JsonKey(GuidKey)] = winrt::to_string(Utils::GuidToString(_guid.value())); + } root[JsonKey(NameKey)] = winrt::to_string(_name); + root[JsonKey(HiddenKey)] = _hidden; ///// Core Settings ///// if (_defaultForeground) @@ -344,155 +353,200 @@ Json::Value Profile::ToJson() const // - a new Profile instance created from the values in `json` Profile Profile::FromJson(const Json::Value& json) { - Profile result{}; + Profile result; - // Profile-specific Settings - if (auto name{ json[JsonKey(NameKey)] }) - { - result._name = GetWstringFromJson(name); - } - if (auto guid{ json[JsonKey(GuidKey)] }) - { - result._guid = Utils::GuidFromString(GetWstringFromJson(guid)); - } - else - { - // Always use the name to generate the temporary GUID. That way, across - // reloads, we'll generate the same static GUID. - const std::wstring_view name = result._name; - result._guid = Utils::CreateV5Uuid(RUNTIME_GENERATED_PROFILE_NAMESPACE_GUID, gsl::as_bytes(gsl::make_span(name))); + result.LayerJson(json); - TraceLoggingWrite( - g_hTerminalAppProvider, - "SynthesizedGuidForProfile", - TraceLoggingDescription("Event emitted when a profile is deserialized without a GUID"), - TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), - TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance)); - } + return result; +} - // Core Settings - if (auto foreground{ json[JsonKey(ForegroundKey)] }) +// Method Description: +// - Returns true if we think the provided json object represents an instance of +// the same object as this object. If true, we should layer that json object +// on us, instead of creating a new object. +// Arguments: +// - json: The json object to query to see if it's the same +// Return Value: +// - true iff the json object has the same `GUID` as we do. +bool Profile::ShouldBeLayered(const Json::Value& json) const +{ + if (!_guid.has_value()) { - const auto color = Utils::ColorFromHexString(foreground.asString()); - result._defaultForeground = color; + return false; } - if (auto background{ json[JsonKey(BackgroundKey)] }) + + if (json.isMember(JsonKey(GuidKey))) { - const auto color = Utils::ColorFromHexString(background.asString()); - result._defaultBackground = color; + const auto guid{ json[JsonKey(GuidKey)] }; + const auto otherGuid = Utils::GuidFromString(GetWstringFromJson(guid)); + return _guid.value() == otherGuid; } - if (auto colorScheme{ json[JsonKey(ColorSchemeKey)] }) + + // TODO: GH#754 - for profiles with a `source`, also check the `source` property. + + return false; +} + +// Method Description: +// - Helper function to convert a json value into a value of the Stretch enum. +// Calls into ParseImageStretchMode. Used with JsonUtils::GetOptionalValue. +// Arguments: +// - json: the Json::Value object to parse. +// Return Value: +// - An appropriate value from Windows.UI.Xaml.Media.Stretch +winrt::Windows::UI::Xaml::Media::Stretch Profile::_ConvertJsonToStretchMode(const Json::Value& json) +{ + return Profile::ParseImageStretchMode(json.asString()); +} + +// Method Description: +// - Helper function to convert a json value into a value of the Stretch enum. +// Calls into ParseImageAlignment. Used with JsonUtils::GetOptionalValue. +// Arguments: +// - json: the Json::Value object to parse. +// Return Value: +// - A pair of HorizontalAlignment and VerticalAlignment +std::tuple Profile::_ConvertJsonToAlignment(const Json::Value& json) +{ + return Profile::ParseImageAlignment(json.asString()); +} + +// Method Description: +// - Layer values from the given json object on top of the existing properties +// of this object. For any keys we're expecting to be able to parse in the +// given object, we'll parse them and replace our settings with values from +// the new json object. Properties that _aren't_ in the json object will _not_ +// be replaced. +// - Optional values in the profile that are set to `null` in the json object +// will be set to nullopt. +// Arguments: +// - json: an object which should be a partial serialization of a Profile object. +// Return Value: +// +void Profile::LayerJson(const Json::Value& json) +{ + // Profile-specific Settings + if (json.isMember(JsonKey(NameKey))) { - result._schemeName = GetWstringFromJson(colorScheme); + auto name{ json[JsonKey(NameKey)] }; + _name = GetWstringFromJson(name); } - else if (auto colorScheme{ json[JsonKey(ColorSchemeKeyOld)] }) + + JsonUtils::GetOptionalGuid(json, GuidKey, _guid); + + if (json.isMember(JsonKey(HiddenKey))) { - // TODO:GH#1069 deprecate old settings key - result._schemeName = GetWstringFromJson(colorScheme); + auto hidden{ json[JsonKey(HiddenKey)] }; + _hidden = hidden.asBool(); } - else if (auto colortable{ json[JsonKey(ColorTableKey)] }) + + // Core Settings + JsonUtils::GetOptionalColor(json, ForegroundKey, _defaultForeground); + + JsonUtils::GetOptionalColor(json, BackgroundKey, _defaultBackground); + + JsonUtils::GetOptionalString(json, ColorSchemeKey, _schemeName); + // TODO:GH#1069 deprecate old settings key + JsonUtils::GetOptionalString(json, ColorSchemeKeyOld, _schemeName); + + // Only look for the "table" if there's no "schemeName" + if (!(json.isMember(JsonKey(ColorSchemeKey))) && + !(json.isMember(JsonKey(ColorSchemeKeyOld))) && + json.isMember(JsonKey(ColorTableKey))) { + auto colortable{ json[JsonKey(ColorTableKey)] }; int i = 0; for (const auto& tableEntry : colortable) { if (tableEntry.isString()) { const auto color = Utils::ColorFromHexString(tableEntry.asString()); - result._colorTable[i] = color; + _colorTable[i] = color; } i++; } } - if (auto historySize{ json[JsonKey(HistorySizeKey)] }) + if (json.isMember(JsonKey(HistorySizeKey))) { + auto historySize{ json[JsonKey(HistorySizeKey)] }; // TODO:MSFT:20642297 - Use a sentinel value (-1) for "Infinite scrollback" - result._historySize = historySize.asInt(); + _historySize = historySize.asInt(); } - if (auto snapOnInput{ json[JsonKey(SnapOnInputKey)] }) + if (json.isMember(JsonKey(SnapOnInputKey))) { - result._snapOnInput = snapOnInput.asBool(); + auto snapOnInput{ json[JsonKey(SnapOnInputKey)] }; + _snapOnInput = snapOnInput.asBool(); } - if (auto cursorColor{ json[JsonKey(CursorColorKey)] }) + if (json.isMember(JsonKey(CursorColorKey))) { + auto cursorColor{ json[JsonKey(CursorColorKey)] }; const auto color = Utils::ColorFromHexString(cursorColor.asString()); - result._cursorColor = color; - } - if (auto cursorHeight{ json[JsonKey(CursorHeightKey)] }) - { - result._cursorHeight = cursorHeight.asUInt(); + _cursorColor = color; } - if (auto cursorShape{ json[JsonKey(CursorShapeKey)] }) + if (json.isMember(JsonKey(CursorHeightKey))) { - result._cursorShape = _ParseCursorShape(GetWstringFromJson(cursorShape)); + auto cursorHeight{ json[JsonKey(CursorHeightKey)] }; + _cursorHeight = cursorHeight.asUInt(); } - if (auto tabTitle{ json[JsonKey(TabTitleKey)] }) + if (json.isMember(JsonKey(CursorShapeKey))) { - result._tabTitle = GetWstringFromJson(tabTitle); + auto cursorShape{ json[JsonKey(CursorShapeKey)] }; + _cursorShape = _ParseCursorShape(GetWstringFromJson(cursorShape)); } + JsonUtils::GetOptionalString(json, TabTitleKey, _tabTitle); // Control Settings - if (auto connectionType{ json[JsonKey(ConnectionTypeKey)] }) - { - result._connectionType = Utils::GuidFromString(GetWstringFromJson(connectionType)); - } - if (auto commandline{ json[JsonKey(CommandlineKey)] }) - { - result._commandline = GetWstringFromJson(commandline); - } - if (auto fontFace{ json[JsonKey(FontFaceKey)] }) - { - result._fontFace = GetWstringFromJson(fontFace); - } - if (auto fontSize{ json[JsonKey(FontSizeKey)] }) - { - result._fontSize = fontSize.asInt(); - } - if (auto acrylicTransparency{ json[JsonKey(AcrylicTransparencyKey)] }) - { - result._acrylicTransparency = acrylicTransparency.asFloat(); - } - if (auto useAcrylic{ json[JsonKey(UseAcrylicKey)] }) - { - result._useAcrylic = useAcrylic.asBool(); - } - if (auto closeOnExit{ json[JsonKey(CloseOnExitKey)] }) - { - result._closeOnExit = closeOnExit.asBool(); - } - if (auto padding{ json[JsonKey(PaddingKey)] }) - { - result._padding = GetWstringFromJson(padding); - } - if (auto scrollbarState{ json[JsonKey(ScrollbarStateKey)] }) + JsonUtils::GetOptionalGuid(json, ConnectionTypeKey, _connectionType); + + if (json.isMember(JsonKey(CommandlineKey))) { - result._scrollbarState = GetWstringFromJson(scrollbarState); + auto commandline{ json[JsonKey(CommandlineKey)] }; + _commandline = GetWstringFromJson(commandline); } - if (auto startingDirectory{ json[JsonKey(StartingDirectoryKey)] }) + if (json.isMember(JsonKey(FontFaceKey))) { - result._startingDirectory = GetWstringFromJson(startingDirectory); + auto fontFace{ json[JsonKey(FontFaceKey)] }; + _fontFace = GetWstringFromJson(fontFace); } - if (auto icon{ json[JsonKey(IconKey)] }) + if (json.isMember(JsonKey(FontSizeKey))) { - result._icon = GetWstringFromJson(icon); + auto fontSize{ json[JsonKey(FontSizeKey)] }; + _fontSize = fontSize.asInt(); } - if (auto backgroundImage{ json[JsonKey(BackgroundImageKey)] }) + if (json.isMember(JsonKey(AcrylicTransparencyKey))) { - result._backgroundImage = GetWstringFromJson(backgroundImage); + auto acrylicTransparency{ json[JsonKey(AcrylicTransparencyKey)] }; + _acrylicTransparency = acrylicTransparency.asFloat(); } - if (auto backgroundImageOpacity{ json[JsonKey(BackgroundImageOpacityKey)] }) + if (json.isMember(JsonKey(UseAcrylicKey))) { - result._backgroundImageOpacity = backgroundImageOpacity.asFloat(); + auto useAcrylic{ json[JsonKey(UseAcrylicKey)] }; + _useAcrylic = useAcrylic.asBool(); } - if (auto backgroundImageStretchMode{ json[JsonKey(BackgroundImageStretchModeKey)] }) + if (json.isMember(JsonKey(CloseOnExitKey))) { - result._backgroundImageStretchMode = ParseImageStretchMode(backgroundImageStretchMode.asString()); + auto closeOnExit{ json[JsonKey(CloseOnExitKey)] }; + _closeOnExit = closeOnExit.asBool(); } - if (auto backgroundImageAlignment{ json[JsonKey(BackgroundImageAlignmentKey)] }) + if (json.isMember(JsonKey(PaddingKey))) { - result._backgroundImageAlignment = ParseImageAlignment(backgroundImageAlignment.asString()); + auto padding{ json[JsonKey(PaddingKey)] }; + _padding = GetWstringFromJson(padding); } - return result; + JsonUtils::GetOptionalString(json, ScrollbarStateKey, _scrollbarState); + + JsonUtils::GetOptionalString(json, StartingDirectoryKey, _startingDirectory); + + JsonUtils::GetOptionalString(json, IconKey, _icon); + + JsonUtils::GetOptionalString(json, BackgroundImageKey, _backgroundImage); + + JsonUtils::GetOptionalDouble(json, BackgroundImageOpacityKey, _backgroundImageOpacity); + + JsonUtils::GetOptionalValue(json, BackgroundImageStretchModeKey, _backgroundImageStretchMode, &Profile::_ConvertJsonToStretchMode); + + JsonUtils::GetOptionalValue(json, BackgroundImageAlignmentKey, _backgroundImageAlignment, &Profile::_ConvertJsonToAlignment); } void Profile::SetFontFace(std::wstring fontFace) noexcept @@ -618,6 +672,19 @@ bool Profile::GetCloseOnExit() const noexcept return _closeOnExit; } +// Method Description: +// - If a profile is marked hidden, it should not appear in the dropdown list of +// profiles. This setting is used to "remove" default and dynamic profiles +// from the list of profiles. +// Arguments: +// - +// Return Value: +// - true iff the profile chould be hidden from the list of profiles. +bool Profile::IsHidden() const noexcept +{ + return _hidden; +} + // Method Description: // - Helper function for expanding any environment variables in a user-supplied starting directory and validating the resulting path // Arguments: @@ -886,3 +953,57 @@ std::wstring_view Profile::_SerializeCursorStyle(const CursorStyle cursorShape) return CursorShapeBar; } } + +// 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 (!_guid.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); + + TraceLoggingWrite( + g_hTerminalAppProvider, + "SynthesizedGuidForProfile", + TraceLoggingDescription("Event emitted when a profile is deserialized without a GUID"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance)); + } +} + +// Function Description: +// - Generates a unique guid for a profile, given the name. For an given name, will always return the same GUID. +// Arguments: +// - name: The name to generate a unique GUID from +// Return Value: +// - a uuidv5 GUID generated from the given name. +GUID Profile::_GenerateGuidForProfile(const std::wstring& name) noexcept +{ + return Utils::CreateV5Uuid(RUNTIME_GENERATED_PROFILE_NAMESPACE_GUID, gsl::as_bytes(gsl::make_span(name))); +} + +// Function Description: +// - Parses the given JSON object to get its GUID. If the json object does not +// have a `guid` set, we'll generate one, using the `name` field. +// Arguments: +// - json: the JSON object to get a GUID from, or generate a unique GUID for +// (given the `name`) +// Return Value: +// - The json's `guid`, or a guid synthesized for it. +GUID Profile::GetGuidOrGenerateForJson(const Json::Value& json) noexcept +{ + std::optional guid{ std::nullopt }; + + JsonUtils::GetOptionalGuid(json, GuidKey, guid); + if (guid) + { + return guid.value(); + } + + auto name = GetWstringFromJson(json[JsonKey(NameKey)]); + return Profile::_GenerateGuidForProfile(name); +} diff --git a/src/cascadia/TerminalApp/Profile.h b/src/cascadia/TerminalApp/Profile.h index 990bbc728c0..2bb7dd68f57 100644 --- a/src/cascadia/TerminalApp/Profile.h +++ b/src/cascadia/TerminalApp/Profile.h @@ -16,6 +16,17 @@ Author(s): #pragma once #include "ColorScheme.h" +// fwdecl unittest classes +namespace TerminalAppLocalTests +{ + class SettingsTests; + class ProfileTests; +}; +namespace TerminalAppUnitTests +{ + class JsonTests; +}; + // GUID used for generating GUIDs at runtime, for profiles that did not have a // GUID specified manually. constexpr GUID RUNTIME_GENERATED_PROFILE_NAMESPACE_GUID = { 0xf65ddb7e, 0x706b, 0x4499, { 0x8a, 0x50, 0x40, 0x31, 0x3c, 0xaf, 0x51, 0x0a } }; @@ -28,8 +39,8 @@ namespace TerminalApp class TerminalApp::Profile final { public: - Profile(const winrt::guid& guid); Profile(); + Profile(const std::optional& guid); ~Profile(); @@ -37,6 +48,8 @@ class TerminalApp::Profile final Json::Value ToJson() const; static Profile FromJson(const Json::Value& json); + bool ShouldBeLayered(const Json::Value& json) const; + void LayerJson(const Json::Value& json); GUID GetGuid() const noexcept; std::wstring_view GetName() const noexcept; @@ -61,21 +74,31 @@ class TerminalApp::Profile final void SetIconPath(std::wstring_view path); bool GetCloseOnExit() const noexcept; + bool IsHidden() const noexcept; + + void GenerateGuidIfNecessary() noexcept; + static GUID GetGuidOrGenerateForJson(const Json::Value& json) noexcept; private: static std::wstring EvaluateStartingDirectory(const std::wstring& directory); static winrt::Microsoft::Terminal::Settings::ScrollbarState ParseScrollbarState(const std::wstring& scrollbarState); static winrt::Windows::UI::Xaml::Media::Stretch ParseImageStretchMode(const std::string_view imageStretchMode); + static winrt::Windows::UI::Xaml::Media::Stretch _ConvertJsonToStretchMode(const Json::Value& json); static std::string_view SerializeImageStretchMode(const winrt::Windows::UI::Xaml::Media::Stretch imageStretchMode); static std::tuple ParseImageAlignment(const std::string_view imageAlignment); + static std::tuple _ConvertJsonToAlignment(const Json::Value& json); + static std::string_view SerializeImageAlignment(const std::tuple imageAlignment); static winrt::Microsoft::Terminal::Settings::CursorStyle _ParseCursorShape(const std::wstring& cursorShapeString); static std::wstring_view _SerializeCursorStyle(const winrt::Microsoft::Terminal::Settings::CursorStyle cursorShape); - GUID _guid; + static GUID _GenerateGuidForProfile(const std::wstring& name) noexcept; + + std::optional _guid{ std::nullopt }; std::wstring _name; std::optional _connectionType; + bool _hidden; // If this is set, then our colors should come from the associated color scheme std::optional _schemeName; @@ -107,4 +130,8 @@ class TerminalApp::Profile final std::wstring _padding; std::optional _icon; + + friend class TerminalAppLocalTests::SettingsTests; + friend class TerminalAppLocalTests::ProfileTests; + friend class TerminalAppUnitTests::JsonTests; }; diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index c26e3282582..00b226ce0f3 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -461,7 +461,13 @@ namespace winrt::TerminalApp::implementation void TerminalPage::_SettingsButtonOnClick(const IInspectable&, const RoutedEventArgs&) { - LaunchSettings(); + const CoreWindow window = CoreWindow::GetForCurrentThread(); + const auto rAltState = window.GetKeyState(VirtualKey::RightMenu); + const auto lAltState = window.GetKeyState(VirtualKey::LeftMenu); + const bool altPressed = WI_IsFlagSet(lAltState, CoreVirtualKeyStates::Down) || + WI_IsFlagSet(rAltState, CoreVirtualKeyStates::Down); + + _LaunchSettings(altPressed); } // Method Description: @@ -1083,16 +1089,11 @@ namespace winrt::TerminalApp::implementation control.PasteTextFromClipboard(); } - void TerminalPage::_OpenSettings() - { - LaunchSettings(); - } - // Function Description: // - Called when the settings button is clicked. ShellExecutes the settings // file, as to open it in the default editor for .json files. Does this in // a background thread, as to not hang/crash the UI thread. - fire_and_forget TerminalPage::LaunchSettings() + fire_and_forget TerminalPage::_LaunchSettings(const bool openDefaults) { // This will switch the execution of the function to a background (not // UI) thread. This is IMPORTANT, because the Windows.Storage API's @@ -1100,7 +1101,8 @@ namespace winrt::TerminalApp::implementation // thread, because the main thread is a STA. co_await winrt::resume_background(); - const auto settingsPath = CascadiaSettings::GetSettingsPath(); + const auto settingsPath = openDefaults ? CascadiaSettings::GetDefaultSettingsPath() : + CascadiaSettings::GetSettingsPath(); HINSTANCE res = ShellExecute(nullptr, nullptr, settingsPath.c_str(), nullptr, nullptr, SW_SHOW); if (static_cast(reinterpret_cast(res)) <= 32) diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 244e8e25ddb..60de55682d5 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -110,8 +110,7 @@ namespace winrt::TerminalApp::implementation void _PasteText(); static fire_and_forget PasteFromClipboard(winrt::Microsoft::Terminal::TerminalControl::PasteFromClipboardEventArgs eventArgs); - void _OpenSettings(); - fire_and_forget LaunchSettings(); + fire_and_forget _LaunchSettings(const bool openDefaults); void _OnTabClick(const IInspectable& sender, const Windows::UI::Xaml::Input::PointerRoutedEventArgs& eventArgs); void _OnTabSelectionChanged(const IInspectable& sender, const Windows::UI::Xaml::Controls::SelectionChangedEventArgs& eventArgs); diff --git a/src/cascadia/TerminalApp/Utils.h b/src/cascadia/TerminalApp/Utils.h index 56e9cb87dd1..bb7c5128944 100644 --- a/src/cascadia/TerminalApp/Utils.h +++ b/src/cascadia/TerminalApp/Utils.h @@ -30,3 +30,26 @@ inline std::string JsonKey(const std::string_view key) } winrt::Windows::UI::Xaml::Controls::IconElement GetColoredIcon(const winrt::hstring& path); + +// This is a pair of helpers for determining if a pair of guids are equal, and +// establishing an ordering on GUIDs (via std::less). +namespace std +{ + template<> + struct less + { + bool operator()(const GUID& lhs, const GUID& rhs) const + { + return memcmp(&lhs, &rhs, sizeof(rhs)) < 0; + } + }; + + template<> + struct equal_to + { + bool operator()(const GUID& lhs, const GUID& rhs) const + { + return memcmp(&lhs, &rhs, sizeof(rhs)) == 0; + } + }; +} diff --git a/src/cascadia/TerminalApp/defaults.json b/src/cascadia/TerminalApp/defaults.json new file mode 100644 index 00000000000..a158ad83389 --- /dev/null +++ b/src/cascadia/TerminalApp/defaults.json @@ -0,0 +1,214 @@ +// THIS IS AN AUTO-GENERATED FILE! Changes to this file will be ignored. +{ + "alwaysShowTabs": true, + "defaultProfile": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}", + "initialCols": 120, + "initialRows": 30, + "requestedTheme": "system", + "showTabsInTitlebar": true, + "showTerminalTitleInTitlebar": true, + "wordDelimiters": " /\\()\"'-.,:;<>~!@#$%^&*|+=[]{}~?\u2502", + + "profiles": + [ + { + "guid": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}", + "name": "Windows PowerShell", + "commandline": "powershell.exe", + "startingDirectory": "%USERPROFILE%", + "background": "#012456", + "closeOnExit": true, + "colorScheme": "Campbell", + "cursorColor": "#FFFFFF", + "cursorShape": "bar", + "fontFace": "Consolas", + "fontSize": 12, + "historySize": 9001, + "icon": "ms-appx:///ProfileIcons/{61c54bbd-c2c6-5271-96e7-009a87ff44bf}.png", + "padding": "8, 8, 8, 8", + "snapOnInput": true, + "useAcrylic": false + }, + { + "guid": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}", + "name": "cmd", + "commandline": "cmd.exe", + "startingDirectory": "%USERPROFILE%", + "closeOnExit": true, + "colorScheme": "Campbell", + "cursorColor": "#FFFFFF", + "cursorShape": "bar", + "fontFace": "Consolas", + "fontSize": 12, + "historySize": 9001, + "icon": "ms-appx:///ProfileIcons/{0caa0dad-35be-5f56-a8ff-afceeeaa6101}.png", + "padding": "8, 8, 8, 8", + "snapOnInput": true, + "useAcrylic": true, + "acrylicOpacity": 0.75 + } + ], + "schemes": + [ + { + "name": "Campbell", + "foreground": "#CCCCCC", + "background": "#0C0C0C", + "black": "#0C0C0C", + "red": "#C50F1F", + "green": "#13A10E", + "yellow": "#C19C00", + "blue": "#0037DA", + "purple": "#881798", + "cyan": "#3A96DD", + "white": "#CCCCCC", + "brightBlack": "#767676", + "brightRed": "#E74856", + "brightGreen": "#16C60C", + "brightYellow": "#F9F1A5", + "brightBlue": "#3B78FF", + "brightPurple": "#B4009E", + "brightCyan": "#61D6D6", + "brightWhite": "#F2F2F2" + }, + { + "name": "Vintage", + "foreground": "#C0C0C0", + "background": "#000000", + "black": "#000000", + "red": "#800000", + "green": "#008000", + "yellow": "#808000", + "blue": "#000080", + "purple": "#800080", + "cyan": "#008080", + "white": "#C0C0C0", + "brightBlack": "#808080", + "brightRed": "#FF0000", + "brightGreen": "#00FF00", + "brightYellow": "#FFFF00", + "brightBlue": "#0000FF", + "brightPurple": "#FF00FF", + "brightCyan": "#00FFFF", + "brightWhite": "#FFFFFF" + }, + { + "name": "One Half Dark", + "foreground": "#DCDFE4", + "background": "#282C34", + "black": "#282C34", + "red": "#E06C75", + "green": "#98C379", + "yellow": "#E5C07B", + "blue": "#61AFEF", + "purple": "#C678DD", + "cyan": "#56B6C2", + "white": "#DCDFE4", + "brightBlack": "#5A6374", + "brightRed": "#E06C75", + "brightGreen": "#98C379", + "brightYellow": "#E5C07B", + "brightBlue": "#61AFEF", + "brightPurple": "#C678DD", + "brightCyan": "#56B6C2", + "brightWhite": "#DCDFE4" + }, + { + "name": "One Half Light", + "foreground": "#383A42", + "background": "#FAFAFA", + "black": "#383A42", + "red": "#E45649", + "green": "#50A14F", + "yellow": "#C18301", + "blue": "#0184BC", + "purple": "#A626A4", + "cyan": "#0997B3", + "white": "#FAFAFA", + "brightBlack": "#4F525D", + "brightRed": "#DF6C75", + "brightGreen": "#98C379", + "brightYellow": "#E4C07A", + "brightBlue": "#61AFEF", + "brightPurple": "#C577DD", + "brightCyan": "#56B5C1", + "brightWhite": "#FFFFFF" + }, + { + "name": "Solarized Dark", + "foreground": "#839496", + "background": "#002B36", + "black": "#073642", + "red": "#DC322F", + "green": "#859900", + "yellow": "#B58900", + "blue": "#268BD2", + "purple": "#D33682", + "cyan": "#2AA198", + "white": "#EEE8D5", + "brightBlack": "#002B36", + "brightRed": "#CB4B16", + "brightGreen": "#586E75", + "brightYellow": "#657B83", + "brightBlue": "#839496", + "brightPurple": "#6C71C4", + "brightCyan": "#93A1A1", + "brightWhite": "#FDF6E3" + }, + { + "name": "Solarized Light", + "foreground": "#657B83", + "background": "#FDF6E3", + "black": "#073642", + "red": "#DC322F", + "green": "#859900", + "yellow": "#B58900", + "blue": "#268BD2", + "purple": "#D33682", + "cyan": "#2AA198", + "white": "#EEE8D5", + "brightBlack": "#002B36", + "brightRed": "#CB4B16", + "brightGreen": "#586E75", + "brightYellow": "#657B83", + "brightBlue": "#839496", + "brightPurple": "#6C71C4", + "brightCyan": "#93A1A1", + "brightWhite": "#FDF6E3" + } + ], + "keybindings": + [ + { "command": "closePane", "keys": ["ctrl+shift+w"] }, + { "command": "copy", "keys": ["ctrl+shift+c"] }, + { "command": "duplicateTab", "keys": ["ctrl+shift+d"] }, + { "command": "newTab", "keys": ["ctrl+shift+t"] }, + { "command": "newTabProfile0", "keys": ["ctrl+shift+1"] }, + { "command": "newTabProfile1", "keys": ["ctrl+shift+2"] }, + { "command": "newTabProfile2", "keys": ["ctrl+shift+3"] }, + { "command": "newTabProfile3", "keys": ["ctrl+shift+4"] }, + { "command": "newTabProfile4", "keys": ["ctrl+shift+5"] }, + { "command": "newTabProfile5", "keys": ["ctrl+shift+6"] }, + { "command": "newTabProfile6", "keys": ["ctrl+shift+7"] }, + { "command": "newTabProfile7", "keys": ["ctrl+shift+8"] }, + { "command": "newTabProfile8", "keys": ["ctrl+shift+9"] }, + { "command": "nextTab", "keys": ["ctrl+tab"] }, + { "command": "openNewTabDropdown", "keys": ["ctrl+shift+space"] }, + { "command": "openSettings", "keys": ["ctrl+,"] }, + { "command": "paste", "keys": ["ctrl+shift+v"] }, + { "command": "prevTab", "keys": ["ctrl+shift+tab"] }, + { "command": "scrollDown", "keys": ["ctrl+shift+down"] }, + { "command": "scrollDownPage", "keys": ["ctrl+shift+pgdn"] }, + { "command": "scrollUp", "keys": ["ctrl+shift+up"] }, + { "command": "scrollUpPage", "keys": ["ctrl+shift+pgup"] }, + { "command": "switchToTab0", "keys": ["ctrl+alt+1"] }, + { "command": "switchToTab1", "keys": ["ctrl+alt+2"] }, + { "command": "switchToTab2", "keys": ["ctrl+alt+3"] }, + { "command": "switchToTab3", "keys": ["ctrl+alt+4"] }, + { "command": "switchToTab4", "keys": ["ctrl+alt+5"] }, + { "command": "switchToTab5", "keys": ["ctrl+alt+6"] }, + { "command": "switchToTab6", "keys": ["ctrl+alt+7"] }, + { "command": "switchToTab7", "keys": ["ctrl+alt+8"] }, + { "command": "switchToTab8", "keys": ["ctrl+alt+9"] } + ] +} diff --git a/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj b/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj index b53faad8eb4..daa23082af3 100644 --- a/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj +++ b/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj @@ -65,8 +65,8 @@ - + @@ -106,6 +106,7 @@ + @@ -319,4 +320,22 @@ + + + + + + + + + + diff --git a/src/cascadia/TerminalApp/userDefaults.json b/src/cascadia/TerminalApp/userDefaults.json new file mode 100644 index 00000000000..1e4f55ce42d --- /dev/null +++ b/src/cascadia/TerminalApp/userDefaults.json @@ -0,0 +1,29 @@ +// To view the default settings, hold "alt" while clicking on the "Settings" button. +// For documentation on these settings, see: https://aka.ms/terminal-documentation + +{ + "defaultProfile": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}", + + "profiles": + [ + { + // Make changes here to the powershell.exe profile + "guid": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}", + "name": "Windows PowerShell", + "commandline": "powershell.exe" + }, + { + // Make changes here to the cmd.exe profile + "guid": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}", + "name": "cmd", + "commandline": "cmd.exe" + } + ], + + // Add custom color schemes to this array + "schemes": [], + + // Add any keybinding overrides to this array. + // To unbind a default keybinding, set the command to "unbound" + "keybindings": [] +} diff --git a/src/cascadia/ut_app/JsonTests.cpp b/src/cascadia/ut_app/JsonTests.cpp index 1e35fae16d4..e7c7ac8eaad 100644 --- a/src/cascadia/ut_app/JsonTests.cpp +++ b/src/cascadia/ut_app/JsonTests.cpp @@ -22,7 +22,6 @@ namespace TerminalAppUnitTests TEST_METHOD(ParseInvalidJson); TEST_METHOD(ParseSimpleColorScheme); TEST_METHOD(ProfileGeneratesGuid); - TEST_METHOD(GeneratedGuidRoundtrips); TEST_CLASS_SETUP(ClassSetup) { @@ -103,9 +102,16 @@ namespace TerminalAppUnitTests void JsonTests::ProfileGeneratesGuid() { - // Parse some profiles without guids. We should generate new guids for - // them. The null guid _is_ a valid guid, so we won't re-generate that - // guid. null is _not_ a valid guid, so we'll regenerate that. + // 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 + // 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 + + // See SettingsTests::ValidateProfilesGenerateGuids for a version of + // this test that includes synthesizing GUIDS for profiles without GUIDs + // set const std::string profileWithoutGuid{ R"({ "name" : "profile0" @@ -140,42 +146,14 @@ namespace TerminalAppUnitTests const GUID cmdGuid = Utils::GuidFromString(L"{6239a42c-1de4-49a3-80bd-e8fdd045185c}"); const GUID nullGuid{ 0 }; - VERIFY_ARE_EQUAL(profile4.GetGuid(), cmdGuid); + VERIFY_IS_FALSE(profile0._guid.has_value()); + VERIFY_IS_FALSE(profile1._guid.has_value()); + VERIFY_IS_FALSE(profile2._guid.has_value()); + VERIFY_IS_TRUE(profile3._guid.has_value()); + VERIFY_IS_TRUE(profile4._guid.has_value()); - VERIFY_ARE_NOT_EQUAL(profile0.GetGuid(), nullGuid); - VERIFY_ARE_NOT_EQUAL(profile1.GetGuid(), nullGuid); - VERIFY_ARE_NOT_EQUAL(profile2.GetGuid(), nullGuid); VERIFY_ARE_EQUAL(profile3.GetGuid(), nullGuid); - - VERIFY_ARE_NOT_EQUAL(profile0.GetGuid(), cmdGuid); - VERIFY_ARE_NOT_EQUAL(profile1.GetGuid(), cmdGuid); - VERIFY_ARE_NOT_EQUAL(profile2.GetGuid(), cmdGuid); - - VERIFY_ARE_NOT_EQUAL(profile0.GetGuid(), profile1.GetGuid()); - VERIFY_ARE_NOT_EQUAL(profile2.GetGuid(), profile1.GetGuid()); - } - - void JsonTests::GeneratedGuidRoundtrips() - { - // Parse a profile without a guid. - // We should automatically generate a GUID for that profile. - // When that profile is serialized and deserialized again, the GUID we - // generated for it should persist. - const std::string profileWithoutGuid{ R"({ - "name" : "profile0" - })" }; - const auto profile0Json = VerifyParseSucceeded(profileWithoutGuid); - - const auto profile0 = Profile::FromJson(profile0Json); - const GUID nullGuid{ 0 }; - - VERIFY_ARE_NOT_EQUAL(profile0.GetGuid(), nullGuid); - - const auto serializedProfile = profile0.ToJson(); - - const auto profile1 = Profile::FromJson(serializedProfile); - - VERIFY_ARE_EQUAL(profile1.GetGuid(), profile0.GetGuid()); + VERIFY_ARE_EQUAL(profile4.GetGuid(), cmdGuid); } } diff --git a/tools/GenerateHeaderForJson.ps1 b/tools/GenerateHeaderForJson.ps1 new file mode 100644 index 00000000000..25128252749 --- /dev/null +++ b/tools/GenerateHeaderForJson.ps1 @@ -0,0 +1,28 @@ +# This script is used for taking a json file and stamping it into a header with +# the contents of that json files as a constexpr string_view in the header. + +param ( + [parameter(Mandatory=$true, Position=0)] + [string]$JsonFile, + + [parameter(Mandatory=$true, Position=1)] + [string]$OutPath, + + [parameter(Mandatory=$true, Position=2)] + [string]$VariableName +) + +# Load the xml files. +$jsonData = Get-Content $JsonFile + +Write-Output "// Copyright (c) Microsoft Corporation" | Out-File -FilePath $OutPath -Encoding ASCII +Write-Output "// Licensed under the MIT license." | Out-File -FilePath $OutPath -Encoding ASCII -Append +Write-Output "" | Out-File -FilePath $OutPath -Encoding ASCII -Append +Write-Output "// THIS IS AN AUTO-GENERATED FILE" | Out-File -FilePath $OutPath -Encoding ASCII -Append +Write-Output "// Generated from " | Out-File -FilePath $OutPath -Encoding ASCII -Append -NoNewline +$fullPath = Resolve-Path -Path $JsonFile +Write-Output $fullPath.Path | Out-File -FilePath $OutPath -Encoding ASCII -Append +Write-Output "constexpr std::string_view $($VariableName){ R`"(" | Out-File -FilePath $OutPath -Encoding ASCII -Append +Write-Output $jsonData | Out-File -FilePath $OutPath -Encoding ASCII -Append +Write-Output ")`" };" | Out-File -FilePath $OutPath -Encoding ASCII -Append +