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:
parent
ed87689c04
commit
8ba8f35dc5
|
@ -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"`
|
||||
|
|
|
@ -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" />
|
||||
|
|
234
src/cascadia/LocalTests_TerminalApp/ColorSchemeTests.cpp
Normal file
234
src/cascadia/LocalTests_TerminalApp/ColorSchemeTests.cpp
Normal 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);
|
||||
}
|
||||
}
|
36
src/cascadia/LocalTests_TerminalApp/JsonTestClass.h
Normal file
36
src/cascadia/LocalTests_TerminalApp/JsonTestClass.h
Normal 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;
|
||||
};
|
159
src/cascadia/LocalTests_TerminalApp/KeyBindingsTests.cpp
Normal file
159
src/cascadia/LocalTests_TerminalApp/KeyBindingsTests.cpp
Normal 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());
|
||||
}
|
||||
}
|
296
src/cascadia/LocalTests_TerminalApp/ProfileTests.cpp
Normal file
296
src/cascadia/LocalTests_TerminalApp/ProfileTests.cpp
Normal 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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -75,7 +75,6 @@ namespace winrt::TerminalApp::implementation
|
|||
|
||||
[[nodiscard]] HRESULT _TryLoadSettings() noexcept;
|
||||
void _LoadSettings();
|
||||
void _OpenSettings();
|
||||
void _RegisterSettingsChange();
|
||||
fire_and_forget _DispatchReloadSettings();
|
||||
void _ReloadSettings();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
};
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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)];
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
59
src/cascadia/TerminalApp/JsonUtils.cpp
Normal file
59
src/cascadia/TerminalApp/JsonUtils.cpp
Normal 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);
|
||||
}
|
69
src/cascadia/TerminalApp/JsonUtils.h
Normal file
69
src/cascadia/TerminalApp/JsonUtils.h
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
214
src/cascadia/TerminalApp/defaults.json
Normal file
214
src/cascadia/TerminalApp/defaults.json
Normal 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"] }
|
||||
]
|
||||
}
|
|
@ -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 '"Generated Files\defaults.h"' -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 '"Generated Files\userDefaults.h"' -VariableName UserSettingsJson" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
|
|
29
src/cascadia/TerminalApp/userDefaults.json
Normal file
29
src/cascadia/TerminalApp/userDefaults.json
Normal 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": []
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
28
tools/GenerateHeaderForJson.ps1
Normal file
28
tools/GenerateHeaderForJson.ps1
Normal 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
|
||||
|
Loading…
Reference in a new issue