diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index cf19440c293..aca348d7135 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -322,288 +322,304 @@ ], "type": "object" }, - "ProfileList": { - "description": "Properties are specific to each unique profile.", - "items": { - "additionalProperties": false, - "properties": { - "acrylicOpacity": { - "default": 0.5, - "description": "When useAcrylic is set to true, it sets the transparency of the window for the profile. Accepts floating point values from 0-1 (default 0.5).", - "maximum": 1, - "minimum": 0, - "type": "number" - }, - "background": { - "$ref": "#/definitions/Color", - "description": "Sets the background color of the profile. Overrides background set in color scheme if colorscheme is set. Uses hex color format: \"#rrggbb\". Default \"#000000\" (black).", - "type": ["string", "null"] - }, - "backgroundImage": { - "description": "Sets the file location of the Image to draw over the window background.", - "type": "string" - }, - "backgroundImageAlignment": { - "default": "center", - "enum": [ - "bottom", - "bottomLeft", - "bottomRight", - "center", - "left", - "right", - "top", - "topLeft", - "topRight" - ], - "type": "string" - }, - "backgroundImageOpacity": { - "description": "(Not in SettingsSchema.md)", - "maximum": 1, - "minimum": 0, - "type": "number" - }, - "backgroundImageStretchMode": { - "default": "uniformToFill", - "description": "Sets how the background image is resized to fill the window.", - "enum": [ - "fill", - "none", - "uniform", - "uniformToFill" - ], - "type": "string" - }, - "closeOnExit": { - "default": "graceful", - "description": "Sets how the profile reacts to termination or failure to launch. Possible values: \"graceful\" (close when exit is typed or the process exits normally), \"always\" (always close) and \"never\" (never close). true and false are accepted as synonyms for \"graceful\" and \"never\" respectively.", - "oneOf": [ - { - "enum": [ - "never", - "graceful", - "always" - ], - "type": "string" + "Profile": { + "description": "Properties specific to a unique profile.", + "additionalProperties": false, + "properties": { + "acrylicOpacity": { + "default": 0.5, + "description": "When useAcrylic is set to true, it sets the transparency of the window for the profile. Accepts floating point values from 0-1 (default 0.5).", + "maximum": 1, + "minimum": 0, + "type": "number" + }, + "background": { + "$ref": "#/definitions/Color", + "description": "Sets the background color of the profile. Overrides background set in color scheme if colorscheme is set. Uses hex color format: \"#rrggbb\". Default \"#000000\" (black).", + "type": ["string", "null"] + }, + "backgroundImage": { + "description": "Sets the file location of the Image to draw over the window background.", + "type": "string" + }, + "backgroundImageAlignment": { + "default": "center", + "enum": [ + "bottom", + "bottomLeft", + "bottomRight", + "center", + "left", + "right", + "top", + "topLeft", + "topRight" + ], + "type": "string" + }, + "backgroundImageOpacity": { + "description": "(Not in SettingsSchema.md)", + "maximum": 1, + "minimum": 0, + "type": "number" + }, + "backgroundImageStretchMode": { + "default": "uniformToFill", + "description": "Sets how the background image is resized to fill the window.", + "enum": [ + "fill", + "none", + "uniform", + "uniformToFill" + ], + "type": "string" + }, + "closeOnExit": { + "default": "graceful", + "description": "Sets how the profile reacts to termination or failure to launch. Possible values: \"graceful\" (close when exit is typed or the process exits normally), \"always\" (always close) and \"never\" (never close). true and false are accepted as synonyms for \"graceful\" and \"never\" respectively.", + "oneOf": [ + { + "enum": [ + "never", + "graceful", + "always" + ], + "type": "string" + }, + { + "type": "boolean" + } + ] + }, + "colorScheme": { + "default": "Campbell", + "description": "Name of the terminal color scheme to use. Color schemes are defined under \"schemes\".", + "type": "string" + }, + "colorTable": { + "description": "Array of colors used in the profile if colorscheme is not set. Colors use hex color format: \"#rrggbb\". Ordering is as follows: [black, red, green, yellow, blue, magenta, cyan, white, bright black, bright red, bright green, bright yellow, bright blue, bright magenta, bright cyan, bright white]", + "items": { + "additionalProperties": false, + "properties": { + "background": { + "$ref": "#/definitions/Color", + "description": "Sets the background color of the color table." }, - { - "type": "boolean" - } - ] - }, - "colorScheme": { - "default": "Campbell", - "description": "Name of the terminal color scheme to use. Color schemes are defined under \"schemes\".", - "type": "string" - }, - "colorTable": { - "description": "Array of colors used in the profile if colorscheme is not set. Colors use hex color format: \"#rrggbb\". Ordering is as follows: [black, red, green, yellow, blue, magenta, cyan, white, bright black, bright red, bright green, bright yellow, bright blue, bright magenta, bright cyan, bright white]", - "items": { - "additionalProperties": false, - "properties": { - "background": { - "$ref": "#/definitions/Color", - "description": "Sets the background color of the color table." - }, - "black": { - "$ref": "#/definitions/Color", - "description": "Sets the color used as ANSI black." - }, - "blue": { - "$ref": "#/definitions/Color", - "description": "Sets the color used as ANSI blue." - }, - "brightBlack": { - "$ref": "#/definitions/Color", - "description": "Sets the color used as ANSI bright black." - }, - "brightBlue": { - "$ref": "#/definitions/Color", - "description": "Sets the color used as ANSI bright blue." - }, - "brightCyan": { - "$ref": "#/definitions/Color", - "description": "Sets the color used as ANSI bright cyan." - }, - "brightGreen": { - "$ref": "#/definitions/Color", - "description": "Sets the color used as ANSI bright green." - }, - "brightPurple": { - "$ref": "#/definitions/Color", - "description": "Sets the color used as ANSI bright purple." - }, - "brightRed": { - "$ref": "#/definitions/Color", - "description": "Sets the color used as ANSI bright red." - }, - "brightWhite": { - "$ref": "#/definitions/Color", - "description": "Sets the color used as ANSI bright white." - }, - "brightYellow": { - "$ref": "#/definitions/Color", - "description": "Sets the color used as ANSI bright yellow." - }, - "cyan": { - "$ref": "#/definitions/Color", - "description": "Sets the color used as ANSI cyan." - }, - "foreground": { - "$ref": "#/definitions/Color", - "description": "Sets the foreground color of the color table." - }, - "green": { - "$ref": "#/definitions/Color", - "description": "Sets the color used as ANSI green." - }, - "purple": { - "$ref": "#/definitions/Color", - "description": "Sets the color used as ANSI purple." - }, - "red": { - "$ref": "#/definitions/Color", - "description": "Sets the color used as ANSI red." - }, - "white": { - "$ref": "#/definitions/Color", - "description": "Sets the color used as ANSI white." - }, - "yellow": { - "$ref": "#/definitions/Color", - "description": "Sets the color used as ANSI yellow." - } + "black": { + "$ref": "#/definitions/Color", + "description": "Sets the color used as ANSI black." + }, + "blue": { + "$ref": "#/definitions/Color", + "description": "Sets the color used as ANSI blue." + }, + "brightBlack": { + "$ref": "#/definitions/Color", + "description": "Sets the color used as ANSI bright black." + }, + "brightBlue": { + "$ref": "#/definitions/Color", + "description": "Sets the color used as ANSI bright blue." + }, + "brightCyan": { + "$ref": "#/definitions/Color", + "description": "Sets the color used as ANSI bright cyan." + }, + "brightGreen": { + "$ref": "#/definitions/Color", + "description": "Sets the color used as ANSI bright green." + }, + "brightPurple": { + "$ref": "#/definitions/Color", + "description": "Sets the color used as ANSI bright purple." + }, + "brightRed": { + "$ref": "#/definitions/Color", + "description": "Sets the color used as ANSI bright red." + }, + "brightWhite": { + "$ref": "#/definitions/Color", + "description": "Sets the color used as ANSI bright white." + }, + "brightYellow": { + "$ref": "#/definitions/Color", + "description": "Sets the color used as ANSI bright yellow." }, - "type": "object" + "cyan": { + "$ref": "#/definitions/Color", + "description": "Sets the color used as ANSI cyan." + }, + "foreground": { + "$ref": "#/definitions/Color", + "description": "Sets the foreground color of the color table." + }, + "green": { + "$ref": "#/definitions/Color", + "description": "Sets the color used as ANSI green." + }, + "purple": { + "$ref": "#/definitions/Color", + "description": "Sets the color used as ANSI purple." + }, + "red": { + "$ref": "#/definitions/Color", + "description": "Sets the color used as ANSI red." + }, + "white": { + "$ref": "#/definitions/Color", + "description": "Sets the color used as ANSI white." + }, + "yellow": { + "$ref": "#/definitions/Color", + "description": "Sets the color used as ANSI yellow." + } }, - "type": "array" - }, - "commandline": { - "description": "Executable used in the profile.", - "type": "string" - }, - "connectionType": { - "$ref": "#/definitions/ProfileGuid", - "description": "A GUID reference to a connection type. Currently undocumented as of 0.3, this is used for Azure Cloud Shell" - }, - "cursorColor": { - "$ref": "#/definitions/Color", - "default": "#FFFFFF", - "description": "Sets the cursor color for the profile. Uses hex color format: \"#rrggbb\"." - }, - "cursorHeight": { - "description": "Sets the percentage height of the cursor starting from the bottom. Only works when cursorShape is set to \"vintage\". Accepts values from 25-100.", - "maximum": 100, - "minimum": 25, - "type": "integer" - }, - "cursorShape": { - "default": "bar", - "description": "Sets the cursor shape for the profile. Possible values: \"vintage\" ( ▃ ), \"bar\" ( ┃, default ), \"underscore\" ( ▁ ), \"filledBox\" ( █ ), \"emptyBox\" ( ▯ )", - "enum": [ - "bar", - "emptyBox", - "filledBox", - "underscore", - "vintage" - ], - "type": "string" - }, - "fontFace": { - "default": "Consolas", - "description": "Name of the font face used in the profile.", - "type": "string" - }, - "fontSize": { - "default": 12, - "description": "Sets the font size.", - "minimum": 1, - "type": "integer" - }, - "foreground": { - "$ref": "#/definitions/Color", - "description": "Sets the foreground color of the profile. Overrides foreground set in color scheme if colorscheme is set. Uses hex color format: \"#rrggbb\". Default \"#ffffff\" (white).", - "type": ["string", "null"] - }, - "guid": { - "$ref": "#/definitions/ProfileGuid", - "description": "Unique identifier of the profile. Written in registry format: \"{00000000-0000-0000-0000-000000000000}\"." - }, - "hidden": { - "default": false, - "description": "If set to true, the profile will not appear in the list of profiles. This can be used to hide default profiles and dynamicially generated profiles, while leaving them in your settings file.", - "type": "boolean" - }, - "historySize": { - "default": 9001, - "description": "The number of lines above the ones displayed in the window you can scroll back to.", - "minimum": -1, - "type": "integer" - }, - "icon": { - "description": "Image file location of the icon used in the profile. Displays within the tab and the dropdown menu.", - "type": "string" - }, - "name": { - "description": "Name of the profile. Displays in the dropdown menu.", - "minLength": 1, - "type": "string" + "type": "object" }, - "padding": { - "default": "8, 8, 8, 8", - "description": "Sets the padding around the text within the window. Can have three different formats: \"#\" sets the same padding for all sides, \"#, #\" sets the same padding for left-right and top-bottom, and \"#, #, #, #\" sets the padding individually for left, top, right, and bottom.", - "pattern": "^-?[0-9]+(\\.[0-9]+)?( *, *-?[0-9]+(\\.[0-9]+)?|( *, *-?[0-9]+(\\.[0-9]+)?){3})?$", - "type": "string" - }, - "scrollbarState": { - "default": "visible", - "description": "Defines the visibility of the scrollbar.", - "enum": [ - "visible", - "hidden" - ], - "type": "string" - }, - "selectionBackground": { - "$ref": "#/definitions/Color", - "description": "Sets the selection background color of the profile. Overrides selection background set in color scheme if colorscheme is set. Uses hex color format: \"#rrggbb\"." - }, - "snapOnInput": { - "default": true, - "description": "When set to true, the window will scroll to the command input line when typing. When set to false, the window will not scroll when you start typing.", - "type": "boolean" - }, - "source": { - "description": "Stores the name of the profile generator that originated this profile.", - "type": "string" - }, - "startingDirectory": { - "description": "The directory the shell starts in when it is loaded.", - "type": "string" - }, - "suppressApplicationTitle": { - "description": "When set to `true`, `tabTitle` overrides the default title of the tab and any title change messages from the application will be suppressed. When set to `false`, `tabTitle` behaves as normal.", - "type": "boolean" - }, - "tabTitle": { - "description": "If set, will replace the name as the title to pass to the shell on startup. Some shells (like bash) may choose to ignore this initial value, while others (cmd, powershell) may use this value over the lifetime of the application.", - "type": "string" - }, - "useAcrylic": { - "default": false, - "description": "When set to true, the window will have an acrylic background. When set to false, the window will have a plain, untextured background.", - "type": "boolean" - } + "type": "array" + }, + "commandline": { + "description": "Executable used in the profile.", + "type": "string" + }, + "connectionType": { + "$ref": "#/definitions/ProfileGuid", + "description": "A GUID reference to a connection type. Currently undocumented as of 0.3, this is used for Azure Cloud Shell" + }, + "cursorColor": { + "$ref": "#/definitions/Color", + "default": "#FFFFFF", + "description": "Sets the cursor color for the profile. Uses hex color format: \"#rrggbb\"." + }, + "cursorHeight": { + "description": "Sets the percentage height of the cursor starting from the bottom. Only works when cursorShape is set to \"vintage\". Accepts values from 25-100.", + "maximum": 100, + "minimum": 25, + "type": "integer" + }, + "cursorShape": { + "default": "bar", + "description": "Sets the cursor shape for the profile. Possible values: \"vintage\" ( ▃ ), \"bar\" ( ┃, default ), \"underscore\" ( ▁ ), \"filledBox\" ( █ ), \"emptyBox\" ( ▯ )", + "enum": [ + "bar", + "emptyBox", + "filledBox", + "underscore", + "vintage" + ], + "type": "string" + }, + "fontFace": { + "default": "Consolas", + "description": "Name of the font face used in the profile.", + "type": "string" + }, + "fontSize": { + "default": 12, + "description": "Sets the font size.", + "minimum": 1, + "type": "integer" + }, + "foreground": { + "$ref": "#/definitions/Color", + "description": "Sets the foreground color of the profile. Overrides foreground set in color scheme if colorscheme is set. Uses hex color format: \"#rrggbb\". Default \"#ffffff\" (white).", + "type": ["string", "null"] + }, + "guid": { + "$ref": "#/definitions/ProfileGuid", + "description": "Unique identifier of the profile. Written in registry format: \"{00000000-0000-0000-0000-000000000000}\"." + }, + "hidden": { + "default": false, + "description": "If set to true, the profile will not appear in the list of profiles. This can be used to hide default profiles and dynamicially generated profiles, while leaving them in your settings file.", + "type": "boolean" + }, + "historySize": { + "default": 9001, + "description": "The number of lines above the ones displayed in the window you can scroll back to.", + "minimum": -1, + "type": "integer" + }, + "icon": { + "description": "Image file location of the icon used in the profile. Displays within the tab and the dropdown menu.", + "type": "string" + }, + "name": { + "description": "Name of the profile. Displays in the dropdown menu.", + "minLength": 1, + "type": "string" + }, + "padding": { + "default": "8, 8, 8, 8", + "description": "Sets the padding around the text within the window. Can have three different formats: \"#\" sets the same padding for all sides, \"#, #\" sets the same padding for left-right and top-bottom, and \"#, #, #, #\" sets the padding individually for left, top, right, and bottom.", + "pattern": "^-?[0-9]+(\\.[0-9]+)?( *, *-?[0-9]+(\\.[0-9]+)?|( *, *-?[0-9]+(\\.[0-9]+)?){3})?$", + "type": "string" + }, + "scrollbarState": { + "default": "visible", + "description": "Defines the visibility of the scrollbar.", + "enum": [ + "visible", + "hidden" + ], + "type": "string" }, + "selectionBackground": { + "$ref": "#/definitions/Color", + "description": "Sets the selection background color of the profile. Overrides selection background set in color scheme if colorscheme is set. Uses hex color format: \"#rrggbb\"." + }, + "snapOnInput": { + "default": true, + "description": "When set to true, the window will scroll to the command input line when typing. When set to false, the window will not scroll when you start typing.", + "type": "boolean" + }, + "source": { + "description": "Stores the name of the profile generator that originated this profile.", + "type": "string" + }, + "startingDirectory": { + "description": "The directory the shell starts in when it is loaded.", + "type": "string" + }, + "suppressApplicationTitle": { + "description": "When set to `true`, `tabTitle` overrides the default title of the tab and any title change messages from the application will be suppressed. When set to `false`, `tabTitle` behaves as normal.", + "type": "boolean" + }, + "tabTitle": { + "description": "If set, will replace the name as the title to pass to the shell on startup. Some shells (like bash) may choose to ignore this initial value, while others (cmd, powershell) may use this value over the lifetime of the application.", + "type": "string" + }, + "useAcrylic": { + "default": false, + "description": "When set to true, the window will have an acrylic background. When set to false, the window will have a plain, untextured background.", + "type": "boolean" + } + }, + "type": "object" + }, + "ProfileList": { + "description": "A list of profiles and the properties specific to each.", + "items": { + "$ref": "#/definitions/Profile", "required": [ "guid", "name" - ], - "type": "object" + ] }, "type": "array" }, + "ProfilesObject": { + "description": "A list of profiles and default settings that apply to all of them", + "properties": { + "list": { + "$ref": "#/definitions/ProfileList" + }, + "defaults": { + "description": "The default settings that apply to every profile.", + "$ref": "#/definitions/Profile" + } + } + }, "SchemeList": { "description": "Properties are specific to each color scheme. ColorTool is a great tool you can use to create and explore new color schemes. All colors use hex color format.", "items": { @@ -703,7 +719,12 @@ { "additionalItems": true, "properties": { - "profiles": { "$ref": "#/definitions/ProfileList" }, + "profiles": { + "oneOf": [ + { "$ref": "#/definitions/ProfileList" }, + { "$ref": "#/definitions/ProfilesObject" } + ] + }, "schemes": { "$ref": "#/definitions/SchemeList" } }, "required": [ @@ -718,7 +739,12 @@ "additionalItems": false, "properties": { "globals": { "$ref": "#/definitions/Globals" }, - "profiles": { "$ref": "#/definitions/ProfileList" }, + "profiles": { + "oneOf": [ + { "$ref": "#/definitions/ProfileList" }, + { "$ref": "#/definitions/ProfilesObject" } + ] + }, "schemes": { "$ref": "#/definitions/SchemeList" } }, "required": [ diff --git a/doc/user-docs/UsingJsonSettings.md b/doc/user-docs/UsingJsonSettings.md index 36a11a5e7c1..7a5af1c8543 100644 --- a/doc/user-docs/UsingJsonSettings.md +++ b/doc/user-docs/UsingJsonSettings.md @@ -199,6 +199,119 @@ like to hide all the WSL profiles, you could add the following setting: ``` +### Default settings + +In [#2325](https://github.com/microsoft/terminal/issues/2325), we introduced the +concept of "Default Profile Settings". These are settings that will apply to all +of your profiles by default. Profiles can still override these settings +individually. With default profile settings, you can easily make changes to all +your profiles at once. For example, given the following settings: + +```json + "defaultProfile": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}", + "profiles": + [ + { + "guid": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}", + "name": "Windows PowerShell", + "commandline": "powershell.exe", + "fontFace": "Cascadia Code", + "fontSize": 14 + }, + { + "guid": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}", + "name": "cmd", + "commandline": "cmd.exe", + "fontFace": "Cascadia Code", + "fontSize": 14 + }, + { + "commandline" : "cmd.exe /k %CMDER_ROOT%\\vendor\\init.bat", + "name" : "cmder", + "startingDirectory" : "%USERPROFILE%", + "fontFace": "Cascadia Code", + "fontSize": 14 + } + ], +``` + +All three of these profiles are using "Cascadia Code" as their `"fontFace"`, and +14 as their `fontSize`. With default profile settings, you can easily set these +properties for all your profiles, like so: + +```json + "defaultProfile": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}", + "profiles": { + "defaults": + { + "fontFace": "Cascadia Code", + "fontSize": 14 + }, + "list": [ + { + "guid": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}", + "name": "Windows PowerShell", + "commandline": "powershell.exe", + }, + { + "guid": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}", + "name": "cmd", + "commandline": "cmd.exe" + }, + { + "commandline" : "cmd.exe /k %CMDER_ROOT%\\vendor\\init.bat", + "name" : "cmder", + "startingDirectory" : "%USERPROFILE%" + } + ], + } +``` + +Note that the `profiles` property has changed in this example from a _list_ of +profiles, to an _object_ with two properties: +* a `list` that contains the list of all the profiles +* the new `defaults` object, which contains all the settings that should apply to + every profile. + +What if I wanted a profile to have a different value for a property other than +the default? Simply set the property in the profile's entry to override the +value from `defaults`. Let's say you want the `cmd` profile to have _"Consolas"_ +as the font, but the rest of your profiles to still have _"Cascadia Code"_. You +could achieve that with the following: + +```json + "defaultProfile": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}", + "profiles": { + "defaults": + { + "fontFace": "Cascadia Code", + "fontSize": 14 + }, + "list": [ + { + "guid": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}", + "name": "Windows PowerShell", + "commandline": "powershell.exe", + }, + { + "guid": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}", + "name": "cmd", + "commandline": "cmd.exe", + "fontFace": "Consolas" + }, + { + "commandline" : "cmd.exe /k %CMDER_ROOT%\\vendor\\init.bat", + "name" : "cmder", + "startingDirectory" : "%USERPROFILE%" + } + ], + } +``` + +In the above settings, the `"fontFace"` in the `cmd.exe` profile overrides the +`"fontFace"` from the `defaults`. + + ## Configuration Examples: ### Add a custom background to the WSL Debian terminal profile diff --git a/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp b/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp index ca3a99e21cf..e7c444b4091 100644 --- a/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp +++ b/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp @@ -8,6 +8,7 @@ #include "JsonTestClass.h" #include "TestUtils.h" #include +#include "../ut_app/TestDynamicProfileGenerator.h" using namespace Microsoft::Console; using namespace TerminalApp; @@ -63,6 +64,10 @@ namespace TerminalAppLocalTests TEST_METHOD(TestCloseOnExitParsing); TEST_METHOD(TestCloseOnExitCompatibilityShim); + TEST_METHOD(TestLayerUserDefaultsBeforeProfiles); + TEST_METHOD(TestDontLayerGuidFromUserDefaults); + TEST_METHOD(TestLayerUserDefaultsOnDynamics); + TEST_METHOD(TestTerminalArgsForBinding); TEST_CLASS_SETUP(ClassSetup) @@ -1548,6 +1553,236 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(CloseOnExitMode::Never, settings._profiles[1].GetCloseOnExitMode()); } + void SettingsTests::TestLayerUserDefaultsBeforeProfiles() + { + // Test for microsoft/terminal#2325. For this test, we'll be setting the + // "historySize" in the "defaultSettings", so it should apply to all + // profiles, unless they override it. In one of the user's profiles, + // we'll override that value, and in the other, we'll leave it + // untouched. + + const std::string settings0String{ R"( + { + "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "profiles": { + "defaults": { + "historySize": 1234 + }, + "list": [ + { + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "name": "profile0", + "historySize": 2345 + }, + { + "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}", + "name": "profile1" + } + ] + } + })" }; + VerifyParseSucceeded(settings0String); + + const auto guid1 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}"); + const auto guid2 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-2222-49a3-80bd-e8fdd045185c}"); + + { + CascadiaSettings settings{ false }; + settings._ParseJsonString(settings0String, false); + VERIFY_IS_TRUE(settings._userDefaultProfileSettings == Json::Value::null); + settings._ApplyDefaultsFromUserSettings(); + VERIFY_IS_FALSE(settings._userDefaultProfileSettings == Json::Value::null); + settings.LayerJson(settings._userSettings); + + VERIFY_ARE_EQUAL(guid1, settings._globals._defaultProfile); + VERIFY_ARE_EQUAL(2u, settings._profiles.size()); + + VERIFY_ARE_EQUAL(2345, settings._profiles.at(0)._historySize); + VERIFY_ARE_EQUAL(1234, settings._profiles.at(1)._historySize); + } + } + + void SettingsTests::TestDontLayerGuidFromUserDefaults() + { + // Test for microsoft/terminal#2325. We don't want the user to put a + // "guid" in the "defaultSettings", and have that apply to all the other + // profiles + + const std::string settings0String{ R"( + { + "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "profiles": { + "defaults": { + "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}" + }, + "list": [ + { + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "name": "profile0", + "historySize": 2345 + }, + { + // Doesn't have a GUID, we'll auto-generate one + "name": "profile1" + } + ] + } + })" }; + VerifyParseSucceeded(settings0String); + + const auto guid1 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}"); + const auto guid2 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-2222-49a3-80bd-e8fdd045185c}"); + + { + CascadiaSettings settings{ false }; + settings._ParseJsonString(DefaultJson, true); + settings.LayerJson(settings._defaultSettings); + VERIFY_ARE_EQUAL(2u, settings._profiles.size()); + + settings._ParseJsonString(settings0String, false); + VERIFY_IS_TRUE(settings._userDefaultProfileSettings == Json::Value::null); + settings._ApplyDefaultsFromUserSettings(); + VERIFY_IS_FALSE(settings._userDefaultProfileSettings == Json::Value::null); + + Log::Comment(NoThrowString().Format( + L"Ensure that cmd and powershell don't get their GUIDs overwritten")); + VERIFY_ARE_NOT_EQUAL(guid2, settings._profiles.at(0)._guid); + VERIFY_ARE_NOT_EQUAL(guid2, settings._profiles.at(1)._guid); + + settings.LayerJson(settings._userSettings); + + VERIFY_ARE_EQUAL(guid1, settings._globals._defaultProfile); + VERIFY_ARE_EQUAL(4u, settings._profiles.size()); + + VERIFY_ARE_EQUAL(guid1, settings._profiles.at(2)._guid); + VERIFY_IS_FALSE(settings._profiles.at(3)._guid.has_value()); + } + } + + void SettingsTests::TestLayerUserDefaultsOnDynamics() + { + // Test for microsoft/terminal#2325. For this test, we'll be setting the + // "historySize" in the "defaultSettings", so it should apply to all + // profiles, unless they override it. The dynamic profiles will _also_ + // set this value, but from discussion in GH#2325, we decided that + // settings in defaultSettings should apply _on top_ of settings from + // dynamic profiles. + + GUID guid1 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}"); + GUID guid2 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-2222-49a3-80bd-e8fdd045185c}"); + GUID guid3 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-3333-49a3-80bd-e8fdd045185c}"); + + const std::string userProfiles{ R"( + { + "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "profiles": { + "defaults": { + "historySize": 1234 + }, + "list": [ + { + "name" : "profile0FromUserSettings", // this is _profiles.at(0) + "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", + "source": "Terminal.App.UnitTest.0" + }, + { + "name" : "profile1FromUserSettings", // this is _profiles.at(2) + "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}", + "source": "Terminal.App.UnitTest.1", + "historySize": 4444 + }, + { + "name" : "profile2FromUserSettings", // this is _profiles.at(3) + "guid": "{6239a42c-3333-49a3-80bd-e8fdd045185c}", + "historySize": 5555 + } + ] + } + })" }; + + auto gen0 = std::make_unique(L"Terminal.App.UnitTest.0"); + gen0->pfnGenerate = [guid1, guid2]() { + std::vector profiles; + Profile p0{ guid1 }; + p0.SetName(L"profile0"); // this is _profiles.at(0) + p0._historySize = 1111; + profiles.push_back(p0); + return profiles; + }; + auto gen1 = std::make_unique(L"Terminal.App.UnitTest.1"); + gen1->pfnGenerate = [guid1, guid2]() { + std::vector profiles; + Profile p0{ guid1 }, p1{ guid2 }; + p0.SetName(L"profile0"); // this is _profiles.at(1) + p1.SetName(L"profile1"); // this is _profiles.at(2) + p0._historySize = 2222; + profiles.push_back(p0); + p1._historySize = 3333; + profiles.push_back(p1); + return profiles; + }; + + CascadiaSettings settings{ false }; + settings._profileGenerators.emplace_back(std::move(gen0)); + settings._profileGenerators.emplace_back(std::move(gen1)); + + Log::Comment(NoThrowString().Format( + L"All profiles with the same name have the same GUID. However, they" + L" will not be layered, because they have different source's")); + + // parse userProfiles as the user settings + settings._ParseJsonString(userProfiles, false); + VERIFY_ARE_EQUAL(0u, settings._profiles.size(), L"Just parsing the user settings doesn't actually layer them"); + settings._LoadDynamicProfiles(); + VERIFY_ARE_EQUAL(3u, settings._profiles.size()); + + VERIFY_ARE_EQUAL(1111, settings._profiles.at(0)._historySize); + VERIFY_ARE_EQUAL(2222, settings._profiles.at(1)._historySize); + VERIFY_ARE_EQUAL(3333, settings._profiles.at(2)._historySize); + + settings._ApplyDefaultsFromUserSettings(); + + VERIFY_ARE_EQUAL(1234, settings._profiles.at(0)._historySize); + VERIFY_ARE_EQUAL(1234, settings._profiles.at(1)._historySize); + VERIFY_ARE_EQUAL(1234, settings._profiles.at(2)._historySize); + + settings.LayerJson(settings._userSettings); + VERIFY_ARE_EQUAL(4u, settings._profiles.size()); + + VERIFY_IS_TRUE(settings._profiles.at(0)._source.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(1)._source.has_value()); + VERIFY_IS_TRUE(settings._profiles.at(2)._source.has_value()); + VERIFY_IS_FALSE(settings._profiles.at(3)._source.has_value()); + + VERIFY_ARE_EQUAL(L"Terminal.App.UnitTest.0", settings._profiles.at(0)._source.value()); + VERIFY_ARE_EQUAL(L"Terminal.App.UnitTest.1", settings._profiles.at(1)._source.value()); + VERIFY_ARE_EQUAL(L"Terminal.App.UnitTest.1", settings._profiles.at(2)._source.value()); + + 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_ARE_EQUAL(guid1, settings._profiles.at(0)._guid.value()); + VERIFY_ARE_EQUAL(guid1, settings._profiles.at(1)._guid.value()); + VERIFY_ARE_EQUAL(guid2, settings._profiles.at(2)._guid.value()); + + VERIFY_ARE_EQUAL(L"profile0FromUserSettings", settings._profiles.at(0)._name); + VERIFY_ARE_EQUAL(L"profile0", settings._profiles.at(1)._name); + VERIFY_ARE_EQUAL(L"profile1FromUserSettings", settings._profiles.at(2)._name); + VERIFY_ARE_EQUAL(L"profile2FromUserSettings", settings._profiles.at(3)._name); + + Log::Comment(NoThrowString().Format( + L"This is the real meat of the test: The two dynamic profiles that " + L"_didn't_ have historySize set in the userSettings should have " + L"1234 as their historySize(from the defaultSettings).The other two" + L" profiles should have their custom historySize value.")); + + VERIFY_ARE_EQUAL(1234, settings._profiles.at(0)._historySize); + VERIFY_ARE_EQUAL(1234, settings._profiles.at(1)._historySize); + VERIFY_ARE_EQUAL(4444, settings._profiles.at(2)._historySize); + VERIFY_ARE_EQUAL(5555, settings._profiles.at(3)._historySize); + } + void SettingsTests::TestTerminalArgsForBinding() { const std::string settingsJson{ R"( diff --git a/src/cascadia/TerminalApp/CascadiaSettings.h b/src/cascadia/TerminalApp/CascadiaSettings.h index b34a9862bce..98384067333 100644 --- a/src/cascadia/TerminalApp/CascadiaSettings.h +++ b/src/cascadia/TerminalApp/CascadiaSettings.h @@ -82,6 +82,7 @@ class TerminalApp::CascadiaSettings final std::string _userSettingsString; Json::Value _userSettings; Json::Value _defaultSettings; + Json::Value _userDefaultProfileSettings{ Json::Value::null }; void _LayerOrCreateProfile(const Json::Value& profileJson); Profile* _FindMatchingProfile(const Json::Value& profileJson); @@ -93,6 +94,8 @@ class TerminalApp::CascadiaSettings final bool _PrependSchemaDirective(); bool _AppendDynamicProfilesToUserSettings(); + void _ApplyDefaultsFromUserSettings(); + void _LoadDynamicProfiles(); static bool _IsPackaged(); diff --git a/src/cascadia/TerminalApp/CascadiaSettingsSerialization.cpp b/src/cascadia/TerminalApp/CascadiaSettingsSerialization.cpp index 4daee90ec8e..634d11eb85b 100644 --- a/src/cascadia/TerminalApp/CascadiaSettingsSerialization.cpp +++ b/src/cascadia/TerminalApp/CascadiaSettingsSerialization.cpp @@ -30,6 +30,8 @@ static constexpr std::wstring_view DefaultsFilename{ L"defaults.json" }; static constexpr std::string_view SchemaKey{ "$schema" }; static constexpr std::string_view ProfilesKey{ "profiles" }; +static constexpr std::string_view DefaultSettingsKey{ "defaults" }; +static constexpr std::string_view ProfilesListKey{ "list" }; static constexpr std::string_view KeybindingsKey{ "keybindings" }; static constexpr std::string_view GlobalsKey{ "globals" }; static constexpr std::string_view SchemesKey{ "schemes" }; @@ -82,6 +84,14 @@ std::unique_ptr CascadiaSettings::LoadAll() // that should be disabled. resultPtr->_LoadDynamicProfiles(); + // See microsoft/terminal#2325: find the defaultSettings from the user's + // settings. Layer those settings upon all the existing profiles we have + // (defaults and dynamic profiles). We'll also set + // _userDefaultProfileSettings here. When we LayerJson below to apply the + // user settings, we'll make sure to use these defaultSettings _before_ any + // profiles the user might have. + resultPtr->_ApplyDefaultsFromUserSettings(); + // Apply the user's settings resultPtr->LayerJson(resultPtr->_userSettings); @@ -491,7 +501,16 @@ void CascadiaSettings::_LayerOrCreateProfile(const Json::Value& profileJson) // `source`. Dynamic profiles _must_ be layered on an existing profile. if (!Profile::IsDynamicProfileObject(profileJson)) { - auto profile = Profile::FromJson(profileJson); + Profile profile{}; + + // GH#2325: If we have a set of default profile settings, apply them here. + // We _won't_ have these settings yet for defaults, dynamic profiles. + if (_userDefaultProfileSettings) + { + profile.LayerJson(_userDefaultProfileSettings); + } + + profile.LayerJson(profileJson); _profiles.emplace_back(profile); } } @@ -524,6 +543,46 @@ Profile* CascadiaSettings::_FindMatchingProfile(const Json::Value& profileJson) return nullptr; } +// Method Description: +// - Finds the "default profile settings" if they exist in the users settings, +// and applies them to the existing profiles. The "default profile settings" +// are settings that should be applied to every profile a user has, with the +// option of being overridden by explicit values in the profile. This should +// be called _after_ the defaults have been parsed and dynamic profiles have +// been generated, but before the other user profiles have been loaded. +// Arguments: +// - +// Return Value: +// - +void CascadiaSettings::_ApplyDefaultsFromUserSettings() +{ + // If `profiles` was an object, then look for the `defaults` object + // underneath it for the default profile settings. + auto defaultSettings{ Json::Value::null }; + + if (const auto profiles{ _userSettings[JsonKey(ProfilesKey)] }) + { + if (profiles.isObject()) + { + defaultSettings = profiles[JsonKey(DefaultSettingsKey)]; + } + } + + if (defaultSettings) + { + _userDefaultProfileSettings = defaultSettings; + + // Remove the `guid` member from the default settings. That'll + // hyper-explode, so just don't let them do that. + _userDefaultProfileSettings.removeMember({ "guid" }); + + for (auto& profile : _profiles) + { + profile.LayerJson(_userDefaultProfileSettings); + } + } +} + // 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 @@ -784,7 +843,10 @@ std::wstring CascadiaSettings::GetDefaultSettingsPath() // - the Json::Value representing the profiles property from the given object const Json::Value& CascadiaSettings::_GetProfilesJsonObject(const Json::Value& json) { - return json[JsonKey(ProfilesKey)]; + const auto& profilesProperty = json[JsonKey(ProfilesKey)]; + return profilesProperty.isArray() ? + profilesProperty : + profilesProperty[JsonKey(ProfilesListKey)]; } // Function Description: