Add Cascading User + Default Settings (#2515)

This PR represents the start of the work on Cascading User + default settings, #754.

Cascading settings will be done in two parts: 
* [ ] Layered Default+User settings (this PR)
* [ ] Dynamic Profile Generation (#2603).

Until _both_ are done, _neither are going in. The dynamic profiles PR will target this PR when it's ready, but will go in as a separate commit into master.

This PR covers adding one primary feature: the settings are now in two separate files:
* a static `defaults.json` that ships with the package (the "default settings")
* a `profiles.json` with the user's customizations (the "user settings)

User settings are _layered_ upon the settings in the defaults settings.

## References

Other things that might be related here:
* #1378 - This seems like it's definitely fixed. The default keybindings are _much_ cleaner, and without the save-on-load behavior, the user's keybindings will be left in a good state 
* #1398 - This might have honestly been solved by #2475 

## PR Checklist
* [x] Closes #754
* [x] Closes #1378 
* [x] Closes #2566
* [x] I work here
* [x] Tests added/passed
* [x] Requires documentation to be updated - it **ABSOLUTELY DOES**


## Detailed Description of the Pull Request / Additional comments

1. We start by taking all of the `FromJson` functions in Profile, ColorScheme, Globals, etc, and converting them to `LayerJson` methods. These are effectively the same, with the change that instead of building a new object, they are simply layering the values on top of `this` object. 
2. Next, we add tests for layering properties like that.
3. Now, we add a `defaults.json` to the package. This is the file the users can refer to as our default settings.
4. We then take that `defaults.json` and stamp it into an auto generated `.h` file, so we can use it's data without having to worry about reading it from disk.
5. We then change the `LoadAll` function in `CascadiaSettings`. Now, the function does two loads - one from the defaults, and then a second load from the `profiles.json` file, layering the settings from each source upon the previous values.
6. If the `profiles.json` file doesn't exist, we'll create it from a hardcoded `userDefaults.json`, which is stamped in similar to how `defaults.json` is.
7. We also add support for _unbinding_ keybindings that might exist in the `defaults.json`, but the user doesn't want to be bound to anything.
8. We add support for _hiding_ a profile, which is useful if a user doesn't want one of the default profiles to appear in the list of profiles.

## TODO:
* [x] Still need to make Alt+Click work on the settings button
* [x] Need to write some user documentation on how the new settings model works
* [x] Fix the pair of tests I broke (re: Duplicate profiles)


<hr>

* Create profiles by layering them

* Update test to layer multiple times on the same profile

* Add support for layering an array of profiles, but break a couple tests

* Add a defaults.json to the package

* Layer colorschemes

  * Moves tests into individual classes
  * adds support for layering a colorscheme on top of another

* Layer an array of color schemes

* oh no, this was missed with #2481

  must have committed without staging this change, uh oh. Not like those tests actually work so nbd

* Layer keybindings

* Read settings from defaults.json + profiles.json, layer appropriately

  This is like 80% of #754. Needs tests.

* Add tests for keybindings

  * add support to unbind a key with `null` or `"unbound"` or `"garbage"`

* Layer or clear optional properties

* Add a helper to get an optional variable for a bunch of different types

  In the end, I think we need to ask _was this worth it_

* Do this with the stretch mode too

* Add back in the GUID check for profiles

* Add some tests for global settings layering

* M A D  W I T H  P O W E R

  Add a MsBuild target to auto-generate a header with the defaults.json as a
  string in the file. That way, we can _always_ load the defaults. Literally impossible to not.

* When the user's profile.json doesn't exist, create it from a template

* Re-order profiles to match the order set in the user's profiles.json

* Add tests for re-ordering profiles to match user ordering

* Add support for hiding profiles using `"hidden": true`

* Use the hardcoded defaults.json for the exception->"use defaults" case

* Somehow I messed up the git submodules?

* woo documentation

* Fix a Terminal.App.Unit.Tests failure

* signed/unsigned is hard

* Use Alt+Settings button to open the default settings

* Missed a signed/unsigned

* Some very preliminary PR feedback

* More PR feedback

  Use the wil helper for the exe path
  Move jsonutils into their own file
  kill some dead code

* Add templates to these bois

* remove some code for generating defaults, reorder defaults.json a tad

* Make guid a std::optional

* Large block of PR feedback

  * Remove some dead code
  * add some comments
  * tag some todos

* stl is love, stl is life

* add `-noprofile`

* Fix the crash that dustin found

* -Encoding ASCII

* Set a profile's default scheme to Campbell

* Fix the tests I regressed

* Update UsingJsonSetting.md to reflect that changes from these PRs

* Change how GenerateGuidForProfile works

* Make AppKeyBindings do its own serialization

* Remove leftover dead code from the previous commit

* Fix up an enormous number of PR nits

* Fix a typo; Update the defaults to match #2378

* Tiny nits

* Some typos, PR nits

* Fix this broken defaults case
This commit is contained in:
Mike Griese 2019-09-16 12:57:10 -07:00 committed by GitHub
parent ed87689c04
commit 8ba8f35dc5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 2789 additions and 705 deletions

View file

@ -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_<randomString>\RoamingState\profiles.json`
The settings are stored in the file `$env:LocalAppData\Packages\Microsoft.WindowsTerminal_<randomString>\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 <kbd>Alt</kbd> 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 <kbd>ctrl+shift+v</kbd> and
<kbd>shift+Insert</kbd> 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 <kbd>Ctrl+C</kbd> 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 <kbd>Ctrl+C</kbd> 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"`

View file

@ -249,6 +249,7 @@
<Content Include="$(OpenConsoleDir)res\terminal\Wide310x150Logo.scale-400.png">
<Link>Images\Wide310x150Logo.scale-400.png</Link>
</Content>
<!-- Profile Icons -->
<Content Include="ProfileIcons\{0caa0dad-35be-5f56-a8ff-afceeeaa6101}.scale-100.png" />
<Content Include="ProfileIcons\{0caa0dad-35be-5f56-a8ff-afceeeaa6101}.scale-200.png" />
<Content Include="ProfileIcons\{574e775e-4f2a-5b96-ac1e-a2962a402336}.scale-100.png" />
@ -259,9 +260,16 @@
<Content Include="ProfileIcons\{9acb9455-ca41-5af7-950f-6bca1bc9722f}.scale-200.png" />
<Content Include="ProfileIcons\{b453ae62-4e3d-5e58-b989-0a998ec441b8}.scale-100.png" />
<Content Include="ProfileIcons\{b453ae62-4e3d-5e58-b989-0a998ec441b8}.scale-200.png" />
<!-- Default Settings -->
<Content Include="$(OpenConsoleDir)src\cascadia\TerminalApp\defaults.json">
<Link>defaults.json</Link>
</Content>
<!-- Resources -->
<PRIResource Include="Resources\en-US\Resources.resw" />
</ItemGroup>
<Import Project="$(OpenConsoleDir)src\wap-common.build.post.props" />
<ItemGroup>
<ProjectReference Include="..\WindowsTerminal\WindowsTerminal.vcxproj" />
<ProjectReference Include="..\..\host\exe\Host.EXE.vcxproj" />

View file

@ -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);
}
}

View file

@ -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::CharReader>(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<Json::CharReader> _reader;
};

View file

@ -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<winrt::TerminalApp::implementation::AppKeyBindings>();
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<winrt::TerminalApp::implementation::AppKeyBindings>();
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<winrt::TerminalApp::implementation::AppKeyBindings>();
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());
}
}

View file

@ -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);
}
}

View file

@ -5,6 +5,8 @@
#include "../TerminalApp/ColorScheme.h"
#include "../TerminalApp/CascadiaSettings.h"
#include "JsonTestClass.h"
#include <defaults.h>
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::CharReader>(Json::CharReaderBuilder::CharReaderBuilder().newCharReader());
InitializeJsonReader();
return true;
}
Json::Value VerifyParseSucceeded(std::string content);
private:
std::unique_ptr<Json::CharReader> 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<size_t>(0), settings->_warnings.size());
VERIFY_ARE_EQUAL(static_cast<size_t>(2), settings->_profiles.size());
CascadiaSettings settings;
settings._profiles.push_back(profile0);
settings._profiles.push_back(profile1);
settings._ValidateNoDuplicateProfiles();
VERIFY_ARE_EQUAL(static_cast<size_t>(0), settings._warnings.size());
VERIFY_ARE_EQUAL(static_cast<size_t>(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<size_t>(1), settings->_warnings.size());
VERIFY_ARE_EQUAL(::TerminalApp::SettingsLoadWarnings::DuplicateProfile, settings->_warnings.at(0));
settings._ValidateNoDuplicateProfiles();
VERIFY_ARE_EQUAL(static_cast<size_t>(1), settings->_profiles.size());
VERIFY_ARE_EQUAL(L"profile0", settings->_profiles.at(0).GetName());
VERIFY_ARE_EQUAL(static_cast<size_t>(1), settings._warnings.size());
VERIFY_ARE_EQUAL(::TerminalApp::SettingsLoadWarnings::DuplicateProfile, settings._warnings.at(0));
VERIFY_ARE_EQUAL(static_cast<size_t>(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<size_t>(1), settings->_warnings.size());
VERIFY_ARE_EQUAL(::TerminalApp::SettingsLoadWarnings::DuplicateProfile, settings->_warnings.at(0));
VERIFY_ARE_EQUAL(static_cast<size_t>(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<size_t>(1), settings._warnings.size());
VERIFY_ARE_EQUAL(::TerminalApp::SettingsLoadWarnings::DuplicateProfile, settings._warnings.at(0));
VERIFY_ARE_EQUAL(static_cast<size_t>(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<size_t>(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<size_t>(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);
}
}

View file

@ -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);

View file

@ -6,11 +6,15 @@
<!-- ========================= Headers ======================== -->
<ItemGroup>
<ClInclude Include="precomp.h" />
<ClInclude Include="JsonTestClass.h" />
</ItemGroup>
<!-- ========================= Cpp Files ======================== -->
<ItemGroup>
<ClCompile Include="SettingsTests.cpp" />
<ClCompile Include="ProfileTests.cpp" />
<ClCompile Include="ColorSchemeTests.cpp" />
<ClCompile Include="KeyBindingsTests.cpp" />
<ClCompile Include="TabTests.cpp" />
<ClCompile Include="precomp.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
@ -127,7 +131,7 @@
Outputs="$(OutDir)$(TargetName).AppxManifest.xml"
DependsOnTargets="_LocalTestsGenerateCombinedManifests">
<Exec Command="powershell.exe ExecutionPolicy Unrestricted $(OpenConsoleDir)\tools\GenerateAppxFromManifest.ps1 -SxSManifest $(OutDir)$(TargetName).manifest -AppxManifestPrototype $(TargetName).AppxManifest.prototype.xml -OutPath $(OutDir)$(TargetName).AppxManifest.xml" />
<Exec Command="powershell.exe -noprofile ExecutionPolicy Unrestricted $(OpenConsoleDir)\tools\GenerateAppxFromManifest.ps1 -SxSManifest $(OutDir)$(TargetName).manifest -AppxManifestPrototype $(TargetName).AppxManifest.prototype.xml -OutPath $(OutDir)$(TargetName).AppxManifest.xml" />
</Target>

View file

@ -413,8 +413,7 @@ namespace winrt::TerminalApp::implementation
if (FAILED(_settingsLoadedResult))
{
_settings = std::make_unique<CascadiaSettings>();
_settings->CreateDefaults();
_settings = CascadiaSettings::LoadDefaults();
}
auto end = std::chrono::high_resolution_clock::now();

View file

@ -75,7 +75,6 @@ namespace winrt::TerminalApp::implementation
[[nodiscard]] HRESULT _TryLoadSettings() noexcept;
void _LoadSettings();
void _OpenSettings();
void _RegisterSettingsChange();
fire_and_forget _DispatchReloadSettings();
void _ReloadSettings();

View file

@ -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);
}

View file

@ -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:
// - <none>
void AppKeyBindings::ClearKeyBinding(const Settings::KeyChord& chord)
{
_keyShortcuts.erase(chord);
}
Microsoft::Terminal::Settings::KeyChord AppKeyBindings::GetKeyBinding(TerminalApp::ShortcutAction const& action)
{
for (auto& kv : _keyShortcuts)

View file

@ -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<winrt::Microsoft::Terminal::Settings::KeyChord, TerminalApp::ShortcutAction, KeyChordHash, KeyChordEquality> _keyShortcuts;
bool _DoAction(ShortcutAction action);
friend class TerminalAppLocalTests::SettingsTests;
friend class TerminalAppLocalTests::KeyBindingsTests;
};
}

View file

@ -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<AppKeyBindings, ActionEventArgs> CopyText;

View file

@ -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 <winrt/Microsoft.Terminal.Settings.h>
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<std::string_view, ShortcutAction, std::less<>> 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;
}
if (commandString && keys)
const auto commandVal = value[JsonKey(CommandKey)];
const auto keys = value[JsonKey(KeysKey)];
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;
// Try matching the command to one we have
const auto found = commandNames.find(commandString.asString());
// 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. 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;
}
}
// 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)
{
SetKeyBinding(action, chord);
}
else
{
continue;
}
// Try parsing the chord
try
{
const auto chord = KeyChordSerialization::FromString(keyChordString);
newBindings.SetKeyBinding(action, chord);
}
catch (...)
{
continue;
ClearKeyBinding(chord);
}
}
catch (...)
{
continue;
}
}
}
return newBindings;
}

View file

@ -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);
};

View file

@ -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<COLORREF>(&campbellTable[0], gsl::narrow<ptrdiff_t>(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<COLORREF>(&vintageTable[0], gsl::narrow<ptrdiff_t>(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<COLORREF>(&oneHalfDarkTable[0], gsl::narrow<ptrdiff_t>(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<COLORREF>(&oneHalfLightTable[0], gsl::narrow<ptrdiff_t>(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<COLORREF>(&solarizedDarkTable[0], gsl::narrow<ptrdiff_t>(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<COLORREF>(&solarizedLightTable[0], gsl::narrow<ptrdiff_t>(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:
// - <none>
// Return Value:
// - <none>
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:
// - <none>
// Return Value:
// - <none>
void CascadiaSettings::_CreateDefaultKeybindings()
{
AppKeyBindings keyBindings = _globals.GetKeybindings();
// Set up some basic default keybindings
keyBindings.SetKeyBinding(ShortcutAction::NewTab,
KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift,
static_cast<int>('T') });
keyBindings.SetKeyBinding(ShortcutAction::OpenNewTabDropdown,
KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift,
static_cast<int>(' ') });
keyBindings.SetKeyBinding(ShortcutAction::DuplicateTab,
KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift,
static_cast<int>('D') });
keyBindings.SetKeyBinding(ShortcutAction::ClosePane,
KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift,
static_cast<int>('W') });
keyBindings.SetKeyBinding(ShortcutAction::CopyText,
KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift,
static_cast<int>('C') });
keyBindings.SetKeyBinding(ShortcutAction::PasteText,
KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift,
static_cast<int>('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<int>('1') });
keyBindings.SetKeyBinding(ShortcutAction::NewTabProfile1,
KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift,
static_cast<int>('2') });
keyBindings.SetKeyBinding(ShortcutAction::NewTabProfile2,
KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift,
static_cast<int>('3') });
keyBindings.SetKeyBinding(ShortcutAction::NewTabProfile3,
KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift,
static_cast<int>('4') });
keyBindings.SetKeyBinding(ShortcutAction::NewTabProfile4,
KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift,
static_cast<int>('5') });
keyBindings.SetKeyBinding(ShortcutAction::NewTabProfile5,
KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift,
static_cast<int>('6') });
keyBindings.SetKeyBinding(ShortcutAction::NewTabProfile6,
KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift,
static_cast<int>('7') });
keyBindings.SetKeyBinding(ShortcutAction::NewTabProfile7,
KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift,
static_cast<int>('8') });
keyBindings.SetKeyBinding(ShortcutAction::NewTabProfile8,
KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift,
static_cast<int>('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<int>('1') });
keyBindings.SetKeyBinding(ShortcutAction::SwitchToTab1,
KeyChord{ KeyModifiers::Alt | KeyModifiers::Ctrl,
static_cast<int>('2') });
keyBindings.SetKeyBinding(ShortcutAction::SwitchToTab2,
KeyChord{ KeyModifiers::Alt | KeyModifiers::Ctrl,
static_cast<int>('3') });
keyBindings.SetKeyBinding(ShortcutAction::SwitchToTab3,
KeyChord{ KeyModifiers::Alt | KeyModifiers::Ctrl,
static_cast<int>('4') });
keyBindings.SetKeyBinding(ShortcutAction::SwitchToTab4,
KeyChord{ KeyModifiers::Alt | KeyModifiers::Ctrl,
static_cast<int>('5') });
keyBindings.SetKeyBinding(ShortcutAction::SwitchToTab5,
KeyChord{ KeyModifiers::Alt | KeyModifiers::Ctrl,
static_cast<int>('6') });
keyBindings.SetKeyBinding(ShortcutAction::SwitchToTab6,
KeyChord{ KeyModifiers::Alt | KeyModifiers::Ctrl,
static_cast<int>('7') });
keyBindings.SetKeyBinding(ShortcutAction::SwitchToTab7,
KeyChord{ KeyModifiers::Alt | KeyModifiers::Ctrl,
static_cast<int>('8') });
keyBindings.SetKeyBinding(ShortcutAction::SwitchToTab8,
KeyChord{ KeyModifiers::Alt | KeyModifiers::Ctrl,
static_cast<int>('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<size_t> indiciesToDelete{};
std::vector<size_t> 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<GUID, GuidEquality> uniqueGuids{};
std::set<GUID> 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:
// - <none>
// Return Value:
// - <none>
void CascadiaSettings::_ReorderProfilesToMatchUserSettingsOrder()
{
std::set<GUID> uniqueGuids;
std::deque<GUID> 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<GUID> 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:
// - <none>
// Return Value:
// - <none>
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());
}

View file

@ -16,7 +16,7 @@ Author(s):
--*/
#pragma once
#include <winrt/Microsoft.Terminal.TerminalControl.h>
#include <winrt/Microsoft.Terminal.TerminalConnection.h>
#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 @@ public:
CascadiaSettings();
~CascadiaSettings();
static std::unique_ptr<CascadiaSettings> LoadDefaults();
static std::unique_ptr<CascadiaSettings> LoadAll();
void SaveAll() const;
@ -53,8 +57,10 @@ public:
Json::Value ToJson() const;
static std::unique_ptr<CascadiaSettings> 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 @@ public:
private:
GlobalAppSettings _globals;
std::vector<Profile> _profiles;
std::vector<TerminalApp::SettingsLoadWarnings> _warnings{};
std::vector<TerminalApp::SettingsLoadWarnings> _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<std::string> _ReadSettings();
static std::optional<std::string> _ReadUserSettings();
static std::optional<std::string> _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 @@ private:
static Profile _CreateDefaultProfile(const std::wstring_view name);
friend class TerminalAppLocalTests::SettingsTests;
friend class TerminalAppLocalTests::ProfileTests;
friend class TerminalAppLocalTests::ColorSchemeTests;
friend class TerminalAppLocalTests::KeyBindingsTests;
};

View file

@ -4,11 +4,19 @@
#include "pch.h"
#include <argb.h>
#include "CascadiaSettings.h"
#include "AppKeyBindingsSerialization.h"
#include "../../types/inc/utils.hpp"
#include "utils.h"
#include "JsonUtils.h"
#include <appmodel.h>
#include <shlobj.h>
// 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,56 +40,95 @@ 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> CascadiaSettings::LoadAll()
{
std::unique_ptr<CascadiaSettings> resultPtr;
std::optional<std::string> fileData = _ReadSettings();
auto resultPtr = LoadDefaults();
std::optional<std::string> 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();
// Ignore UTF-8 BOM
auto actualDataStart = actualData.c_str();
if (actualData.compare(0, Utf8Bom.size(), Utf8Bom) == 0)
{
actualDataStart += Utf8Bom.size();
}
// Parse the json data.
Json::Value root;
std::unique_ptr<Json::CharReader> 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);
// If this throws, the app will catch it and use the default settings (temporarily)
resultPtr->_ValidateSettings();
resultPtr->_LayerJsonString(fileData.value(), false);
}
else
{
resultPtr = std::make_unique<CascadiaSettings>();
resultPtr->CreateDefaults();
// The settings file does not exist. Let's commit one.
resultPtr->SaveAll();
// We didn't find the user settings. We'll need to create a file
// to use as the user defaults.
_WriteSettings(UserSettingsJson);
}
// If this throws, the app will catch it and use the default settings
resultPtr->_ValidateSettings();
return resultPtr;
}
// Function Description:
// - Creates a new CascadiaSettings object initialized with settings from the
// hardcoded defaults.json.
// Arguments:
// - <none>
// Return Value:
// - a unique_ptr to a CascadiaSettings with the settings from defaults.json
std::unique_ptr<CascadiaSettings> CascadiaSettings::LoadDefaults()
{
auto resultPtr = std::make_unique<CascadiaSettings>();
// 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:
// - <none>
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)
{
actualDataStart += Utf8Bom.size();
}
std::string errs; // This string will recieve any error text from failing to parse.
std::unique_ptr<Json::CharReader> reader{ Json::CharReaderBuilder::CharReaderBuilder().newCharReader() };
// Parse the json data into either our defaults or user settings. We'll keep
// these original json values around for later, in case we need to parse
// their raw contents again.
Json::Value& root = isDefaultSettings ? _defaultSettings : _userSettings;
// `parse` will return false if it fails.
if (!reader->parse(actualDataStart, 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));
}
LayerJson(root);
}
// Method Description:
// - Serialize this settings structure, and save it to a file. The location of
// the file changes depending whether we're running as a packaged
@ -137,13 +186,28 @@ Json::Value CascadiaSettings::ToJson() const
// - a new CascadiaSettings instance created from the values in `json`
std::unique_ptr<CascadiaSettings> CascadiaSettings::FromJson(const Json::Value& json)
{
std::unique_ptr<CascadiaSettings> resultPtr = std::make_unique<CascadiaSettings>();
auto resultPtr = std::make_unique<CascadiaSettings>();
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:
// <none>
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> 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:
// - <none>
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:
// - <none>
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<DWORD>(content.size()), 0, 0));
CloseHandle(hOut);
THROW_LAST_ERROR_IF(!WriteFile(hOut.get(), content.data(), gsl::narrow<DWORD>(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<std::string> CascadiaSettings::_ReadSettings()
std::optional<std::string> CascadiaSettings::_ReadUserSettings()
{
const auto pathToSettingsFile{ CascadiaSettings::GetSettingsPath() };
wil::unique_hfile hFile{ CreateFileW(pathToSettingsFile.c_str(),
@ -310,14 +453,26 @@ std::optional<std::string> CascadiaSettings::_ReadSettings()
}
}
return _ReadFile(hFile.get());
}
// Method Description:
// - Reads the content in UTF-8 encoding of the given file using the Win32 APIs
// Arguments:
// - <none>
// 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<std::string> 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<char[]>(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)];
}

View file

@ -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:
// <none>
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

View file

@ -19,6 +19,13 @@ Author(s):
#include <winrt/Microsoft.Terminal.TerminalControl.h>
#include "../../inc/conattrs.hpp"
// fwdecl unittest classes
namespace TerminalAppLocalTests
{
class SettingsTests;
class ColorSchemeTests;
};
namespace TerminalApp
{
class ColorScheme;
@ -35,6 +42,8 @@ public:
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<COLORREF, COLOR_TABLE_SIZE>& GetTable() noexcept;
@ -46,4 +55,7 @@ private:
std::array<COLORREF, COLOR_TABLE_SIZE> _table;
COLORREF _defaultForeground;
COLORREF _defaultBackground;
friend class TerminalAppLocalTests::SettingsTests;
friend class TerminalAppLocalTests::ColorSchemeTests;
};

View file

@ -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<winrt::TerminalApp::implementation::AppKeyBindings>() },
_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:

View file

@ -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 @@ public:
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 @@ public:
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<winrt::TerminalApp::implementation::AppKeyBindings> _keybindings;
std::vector<ColorScheme> _colorSchemes;
@ -80,4 +86,6 @@ private:
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;
};

View file

@ -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<uint32_t>& 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<std::wstring>& 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<GUID>& 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<double>& target)
{
const auto conversionFn = [](const Json::Value& value) -> double {
return value.asFloat();
};
GetOptionalValue(json,
key,
target,
conversionFn);
}

View file

@ -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<uint32_t>& target);
void GetOptionalString(const Json::Value& json,
std::string_view key,
std::optional<std::wstring>& target);
void GetOptionalGuid(const Json::Value& json,
std::string_view key,
std::optional<GUID>& target);
void GetOptionalDouble(const Json::Value& json,
std::string_view key,
std::optional<double>& 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<T(const Json::Value&)> which can be used to
// convert the Json::Value to the appropriate type.
// Return Value:
// - <none>
template<typename T, typename F>
void GetOptionalValue(const Json::Value& json,
std::string_view key,
std::optional<T>& target,
F&& conversion)
{
if (json.isMember(JsonKey(key)))
{
if (auto jsonVal{ json[JsonKey(key)] })
{
target = conversion(jsonVal);
}
else
{
target = std::nullopt;
}
}
}
};

View file

@ -4,6 +4,7 @@
#include "pch.h"
#include "Profile.h"
#include "Utils.h"
#include "JsonUtils.h"
#include "../../types/inc/Utils.hpp"
#include <DefaultSettings.h>
@ -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(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;
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 `GUID` as we do.
bool Profile::ShouldBeLayered(const Json::Value& json) const
{
if (!_guid.has_value())
{
return false;
}
if (json.isMember(JsonKey(GuidKey)))
{
const auto guid{ json[JsonKey(GuidKey)] };
const auto otherGuid = Utils::GuidFromString(GetWstringFromJson(guid));
return _guid.value() == otherGuid;
}
// 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<winrt::Windows::UI::Xaml::HorizontalAlignment, winrt::Windows::UI::Xaml::VerticalAlignment> 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:
// <none>
void Profile::LayerJson(const Json::Value& json)
{
// Profile-specific Settings
if (auto name{ json[JsonKey(NameKey)] })
if (json.isMember(JsonKey(NameKey)))
{
result._name = GetWstringFromJson(name);
auto name{ json[JsonKey(NameKey)] };
_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)));
TraceLoggingWrite(
g_hTerminalAppProvider,
"SynthesizedGuidForProfile",
TraceLoggingDescription("Event emitted when a profile is deserialized without a GUID"),
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance));
JsonUtils::GetOptionalGuid(json, GuidKey, _guid);
if (json.isMember(JsonKey(HiddenKey)))
{
auto hidden{ json[JsonKey(HiddenKey)] };
_hidden = hidden.asBool();
}
// Core Settings
if (auto foreground{ json[JsonKey(ForegroundKey)] })
{
const auto color = Utils::ColorFromHexString(foreground.asString());
result._defaultForeground = color;
}
if (auto background{ json[JsonKey(BackgroundKey)] })
{
const auto color = Utils::ColorFromHexString(background.asString());
result._defaultBackground = color;
}
if (auto colorScheme{ json[JsonKey(ColorSchemeKey)] })
{
result._schemeName = GetWstringFromJson(colorScheme);
}
else if (auto colorScheme{ json[JsonKey(ColorSchemeKeyOld)] })
{
// TODO:GH#1069 deprecate old settings key
result._schemeName = GetWstringFromJson(colorScheme);
}
else if (auto colortable{ json[JsonKey(ColorTableKey)] })
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;
_cursorColor = color;
}
if (auto cursorHeight{ json[JsonKey(CursorHeightKey)] })
if (json.isMember(JsonKey(CursorHeightKey)))
{
result._cursorHeight = cursorHeight.asUInt();
auto cursorHeight{ json[JsonKey(CursorHeightKey)] };
_cursorHeight = cursorHeight.asUInt();
}
if (auto cursorShape{ json[JsonKey(CursorShapeKey)] })
if (json.isMember(JsonKey(CursorShapeKey)))
{
result._cursorShape = _ParseCursorShape(GetWstringFromJson(cursorShape));
}
if (auto tabTitle{ json[JsonKey(TabTitleKey)] })
{
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)] })
JsonUtils::GetOptionalGuid(json, ConnectionTypeKey, _connectionType);
if (json.isMember(JsonKey(CommandlineKey)))
{
result._connectionType = Utils::GuidFromString(GetWstringFromJson(connectionType));
auto commandline{ json[JsonKey(CommandlineKey)] };
_commandline = GetWstringFromJson(commandline);
}
if (auto commandline{ json[JsonKey(CommandlineKey)] })
if (json.isMember(JsonKey(FontFaceKey)))
{
result._commandline = GetWstringFromJson(commandline);
auto fontFace{ json[JsonKey(FontFaceKey)] };
_fontFace = GetWstringFromJson(fontFace);
}
if (auto fontFace{ json[JsonKey(FontFaceKey)] })
if (json.isMember(JsonKey(FontSizeKey)))
{
result._fontFace = GetWstringFromJson(fontFace);
auto fontSize{ json[JsonKey(FontSizeKey)] };
_fontSize = fontSize.asInt();
}
if (auto fontSize{ json[JsonKey(FontSizeKey)] })
if (json.isMember(JsonKey(AcrylicTransparencyKey)))
{
result._fontSize = fontSize.asInt();
auto acrylicTransparency{ json[JsonKey(AcrylicTransparencyKey)] };
_acrylicTransparency = acrylicTransparency.asFloat();
}
if (auto acrylicTransparency{ json[JsonKey(AcrylicTransparencyKey)] })
if (json.isMember(JsonKey(UseAcrylicKey)))
{
result._acrylicTransparency = acrylicTransparency.asFloat();
auto useAcrylic{ json[JsonKey(UseAcrylicKey)] };
_useAcrylic = useAcrylic.asBool();
}
if (auto useAcrylic{ json[JsonKey(UseAcrylicKey)] })
if (json.isMember(JsonKey(CloseOnExitKey)))
{
result._useAcrylic = useAcrylic.asBool();
auto closeOnExit{ json[JsonKey(CloseOnExitKey)] };
_closeOnExit = closeOnExit.asBool();
}
if (auto closeOnExit{ json[JsonKey(CloseOnExitKey)] })
if (json.isMember(JsonKey(PaddingKey)))
{
result._closeOnExit = closeOnExit.asBool();
}
if (auto padding{ json[JsonKey(PaddingKey)] })
{
result._padding = GetWstringFromJson(padding);
}
if (auto scrollbarState{ json[JsonKey(ScrollbarStateKey)] })
{
result._scrollbarState = GetWstringFromJson(scrollbarState);
}
if (auto startingDirectory{ json[JsonKey(StartingDirectoryKey)] })
{
result._startingDirectory = GetWstringFromJson(startingDirectory);
}
if (auto icon{ json[JsonKey(IconKey)] })
{
result._icon = GetWstringFromJson(icon);
}
if (auto backgroundImage{ json[JsonKey(BackgroundImageKey)] })
{
result._backgroundImage = GetWstringFromJson(backgroundImage);
}
if (auto backgroundImageOpacity{ json[JsonKey(BackgroundImageOpacityKey)] })
{
result._backgroundImageOpacity = backgroundImageOpacity.asFloat();
}
if (auto backgroundImageStretchMode{ json[JsonKey(BackgroundImageStretchModeKey)] })
{
result._backgroundImageStretchMode = ParseImageStretchMode(backgroundImageStretchMode.asString());
}
if (auto backgroundImageAlignment{ json[JsonKey(BackgroundImageAlignmentKey)] })
{
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:
// - <none>
// 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> guid{ std::nullopt };
JsonUtils::GetOptionalGuid(json, GuidKey, guid);
if (guid)
{
return guid.value();
}
auto name = GetWstringFromJson(json[JsonKey(NameKey)]);
return Profile::_GenerateGuidForProfile(name);
}

View file

@ -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>& guid);
~Profile();
@ -37,6 +48,8 @@ public:
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 @@ public:
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<winrt::Windows::UI::Xaml::HorizontalAlignment, winrt::Windows::UI::Xaml::VerticalAlignment> ParseImageAlignment(const std::string_view imageAlignment);
static std::tuple<winrt::Windows::UI::Xaml::HorizontalAlignment, winrt::Windows::UI::Xaml::VerticalAlignment> _ConvertJsonToAlignment(const Json::Value& json);
static std::string_view SerializeImageAlignment(const std::tuple<winrt::Windows::UI::Xaml::HorizontalAlignment, winrt::Windows::UI::Xaml::VerticalAlignment> 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> _guid{ std::nullopt };
std::wstring _name;
std::optional<GUID> _connectionType;
bool _hidden;
// If this is set, then our colors should come from the associated color scheme
std::optional<std::wstring> _schemeName;
@ -107,4 +130,8 @@ private:
std::wstring _padding;
std::optional<std::wstring> _icon;
friend class TerminalAppLocalTests::SettingsTests;
friend class TerminalAppLocalTests::ProfileTests;
friend class TerminalAppUnitTests::JsonTests;
};

View file

@ -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<int>(reinterpret_cast<uintptr_t>(res)) <= 32)

View file

@ -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);

View file

@ -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<GUID>
{
bool operator()(const GUID& lhs, const GUID& rhs) const
{
return memcmp(&lhs, &rhs, sizeof(rhs)) < 0;
}
};
template<>
struct equal_to<GUID>
{
bool operator()(const GUID& lhs, const GUID& rhs) const
{
return memcmp(&lhs, &rhs, sizeof(rhs)) == 0;
}
};
}

View file

@ -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"] }
]
}

View file

@ -65,8 +65,8 @@
<ClInclude Include="../GlobalAppSettings.h" />
<ClInclude Include="../Profile.h" />
<ClInclude Include="../CascadiaSettings.h" />
<ClInclude Include="../AppKeyBindingsSerialization.h" />
<ClInclude Include="../KeyChordSerialization.h" />
<ClInclude Include="../JsonUtils.h" />
<ClInclude Include="../Utils.h" />
<ClInclude Include="../TerminalWarnings.h" />
<ClInclude Include="pch.h" />
@ -106,6 +106,7 @@
<ClCompile Include="../CascadiaSettingsSerialization.cpp" />
<ClCompile Include="../AppKeyBindingsSerialization.cpp" />
<ClCompile Include="../KeyChordSerialization.cpp" />
<ClCompile Include="../JsonUtils.cpp" />
<ClCompile Include="../Utils.cpp" />
<ClCompile Include="../ScopedResourceLoader.cpp" />
<ClCompile Include="pch.cpp">
@ -319,4 +320,22 @@
</PackagingOutputs>
</ItemGroup>
</Target>
<!-- This target will take our defaults.json and stamp it into a .h file that
we can include in the code directly. This way, we don't need to worry about
failing to load the default settings at runtime. -->
<Target Name="_TerminalAppGenerateDefaultsH"
Inputs="..\defaults.json"
Outputs="Generated Files\defaults.h"
BeforeTargets="BeforeClCompile">
<Exec Command="powershell.exe -noprofile ExecutionPolicy Unrestricted $(OpenConsoleDir)\tools\GenerateHeaderForJson.ps1 -JsonFile ..\defaults.json -OutPath '&quot;Generated Files\defaults.h&quot;' -VariableName DefaultJson" />
</Target>
<!-- Same as above, but for the default profiles.json template -->
<Target Name="_TerminalAppGenerateUserSettingsH"
Inputs="..\userDefaults.json"
Outputs="Generated Files\userDefaults.h"
BeforeTargets="BeforeClCompile">
<Exec Command="powershell.exe -noprofile ExecutionPolicy Unrestricted $(OpenConsoleDir)\tools\GenerateHeaderForJson.ps1 -JsonFile ..\userDefaults.json -OutPath '&quot;Generated Files\userDefaults.h&quot;' -VariableName UserSettingsJson" />
</Target>
</Project>

View file

@ -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": []
}

View file

@ -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);
}
}

View file

@ -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