Add Dynamic Profile Generators (#2603)

_**This PR targets the #2515 PR**_. It does that for the sake of diffing. When this PR and #2515 are both ready, I'll merge #2515 first, then change the target of this branch, and merge this one.

<!-- Enter a brief description/summary of your PR here. What does it fix/what does it change/how was it tested (even manually, if necessary)? -->
## Summary of the Pull Request

This PR adds support for "dynamic profiles", in accordance with the [Cascading Settings Spec](https://github.com/microsoft/terminal/blob/master/doc/cascadia/Cascading-Default-Settings.md#dynamic-profiles). Currently, we have three types of default profiles that fit the category of dynamic profile generators. These are profiles that we want to create on behalf of the user, but require runtime information to be able to create correctly. Because they require runtime information, we can't ship a static version of these profiles as a part of `defaults.json`. These three profile generators are:
* The Powershell Core generator
* The WSL Distro generator
* The Azure Cloud Shell generator

<!-- Other than the issue solved, is this relevant to any other issues/existing PRs? --> 
## References

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist
* [x] Closes #754
* [x] I work here
* [x] look at all these **Tests**
* [x] Requires documentation to be updated - This is done as part of the parent PR

<!-- Provide a more detailed description of the PR, other things fixed or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

We want to be able to enable the user to edit dynamic profiles that are generated from DPGs. When dynamic profiles are added, we'll add entries for them to the user's `profiles.json`. We do this _without re-serializing_ the settings. Instead, we insert a partial serialization for the profile into the user's settings. 

### Remaining TODOs:
* Make sure that dynamic profiles appear in the right place in the order of profiles -> #2722
* [x] don't serialize the `colorTable` key for dynamic profiles.
* [x] re-parse the user settings string if we've changed it.
*  Handle changing the default profile to pwsh if it exists on first launch, or file a follow-up issue -> #2721

<!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed


<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

* Start dynamically creating profiles

* Give the inbox generators a namespace

  and generally hack this a lot less

* 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

* Serialize the source key

* Make the Azure cloud shell a dynamic profile

* Make the built-in namespaces public

* Add a mechanism for quick-diffing a profile

  This will be used to generate the json snippets for dynamically generated profiles.

* Generate partial serializations of dynamic profiles _not_ in the user settings

* Start writing tests for generating dyn profiles

  * dyn profiles generate GUIDs based on _source
  * we won't run DPGs when they'd disabled?

* Add more DPG tests - TestDontRunDisabledGenerators

* Don't layer profiles with a source that's also different

* Add another test, DoLayerUserProfilesOnDynamicsWhenSourceMatches

* Actually insert new dynamic profiles into the file

* Minor cleanup of `Profile::ShouldBeLayered`

* Migrate legacy profiles gracefully

* using namespace winrt::Windows::UI::Xaml;

* _Only_ layer dynamic profiles from user settings, never create

* Write a test for migrating dynamic profiles

* Comments for dayssssss

* 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

* Don't layer a profile if the json doesn't have a GUID

* Fix a test I unfixed

* get rid of extraneous bois{};

* Piles of PR feedback

* Collection of PR nits

* PR nits

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

* Tiny nits

* In-den-taition!

* Some typos, PR nits

* Fix this broken defaults case

* Apply suggestions from code review

Co-Authored-By: Carlos Zamora <carlos.zamora@microsoft.com>

* PR nits
This commit is contained in:
Mike Griese 2019-09-16 13:34:27 -07:00 committed by GitHub
parent 8ba8f35dc5
commit 0df02343f1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 1789 additions and 339 deletions

View file

@ -491,12 +491,14 @@ namespace TerminalAppLocalTests
L"default profiles in the opposite order of the default ordering."));
CascadiaSettings settings;
settings._LayerJsonString(defaultProfilesString, true);
settings._ParseJsonString(defaultProfilesString, true);
settings.LayerJson(settings._defaultSettings);
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);
settings._ParseJsonString(userProfiles0String, false);
settings.LayerJson(settings._userSettings);
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);
@ -512,12 +514,14 @@ namespace TerminalAppLocalTests
L"Case 2: Make sure all the user's profiles appear before the defaults."));
CascadiaSettings settings;
settings._LayerJsonString(defaultProfilesString, true);
settings._ParseJsonString(defaultProfilesString, true);
settings.LayerJson(settings._defaultSettings);
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);
settings._ParseJsonString(userProfiles1String, false);
settings.LayerJson(settings._userSettings);
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);
@ -588,14 +592,16 @@ namespace TerminalAppLocalTests
{
CascadiaSettings settings;
settings._LayerJsonString(defaultProfilesString, true);
settings._ParseJsonString(defaultProfilesString, true);
settings.LayerJson(settings._defaultSettings);
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);
settings._ParseJsonString(userProfiles0String, false);
settings.LayerJson(settings._userSettings);
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);
@ -611,14 +617,16 @@ namespace TerminalAppLocalTests
{
CascadiaSettings settings;
settings._LayerJsonString(defaultProfilesString, true);
settings._ParseJsonString(defaultProfilesString, true);
settings.LayerJson(settings._defaultSettings);
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);
settings._ParseJsonString(userProfiles1String, false);
settings.LayerJson(settings._userSettings);
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);
@ -788,7 +796,8 @@ namespace TerminalAppLocalTests
const auto settings0Json = VerifyParseSucceeded(settings0String);
CascadiaSettings settings;
settings._LayerJsonString(settings0String, false);
settings._ParseJsonString(settings0String, false);
settings.LayerJson(settings._userSettings);
VERIFY_ARE_EQUAL(2u, settings._profiles.size());
VERIFY_IS_TRUE(settings._profiles.at(0)._guid.has_value());
@ -824,14 +833,16 @@ namespace TerminalAppLocalTests
const auto settings0Json = VerifyParseSucceeded(settings0String);
CascadiaSettings settings;
settings._LayerJsonString(DefaultJson, true);
settings._ParseJsonString(DefaultJson, true);
settings.LayerJson(settings._defaultSettings);
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);
settings._ParseJsonString(settings0String, false);
settings.LayerJson(settings._userSettings);
VERIFY_ARE_EQUAL(4u, settings._profiles.size());
VERIFY_IS_TRUE(settings._profiles.at(0)._guid.has_value());
@ -923,14 +934,16 @@ namespace TerminalAppLocalTests
const auto settings0Json = VerifyParseSucceeded(settings0String);
CascadiaSettings settings;
settings._LayerJsonString(DefaultJson, true);
settings._ParseJsonString(DefaultJson, true);
settings.LayerJson(settings._defaultSettings);
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);
settings._ParseJsonString(settings0String, false);
settings.LayerJson(settings._userSettings);
VERIFY_ARE_EQUAL(4u, settings._profiles.size());
VERIFY_IS_TRUE(settings._profiles.at(0)._guid.has_value());

View file

@ -0,0 +1,48 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include <winrt/Microsoft.Terminal.TerminalConnection.h>
#include "AzureCloudShellGenerator.h"
#include "LegacyProfileGeneratorNamespaces.h"
#include "../../types/inc/utils.hpp"
#include "../../inc/DefaultSettings.h"
#include "Utils.h"
#include "DefaultProfileUtils.h"
using namespace ::TerminalApp;
std::wstring_view AzureCloudShellGenerator::GetNamespace()
{
return AzureGeneratorNamespace;
}
// Method Description:
// - Checks if the Azure Cloud shell is available on this platform, and if it
// is, creates a profile to be able to launch it.
// Arguments:
// - <none>
// Return Value:
// - a vector with the Azure Cloud Shell connection profile, if available.
std::vector<TerminalApp::Profile> AzureCloudShellGenerator::GenerateProfiles()
{
std::vector<TerminalApp::Profile> profiles;
if (winrt::Microsoft::Terminal::TerminalConnection::AzureConnection::IsAzureConnectionAvailable())
{
auto azureCloudShellProfile{ CreateDefaultProfile(L"Azure Cloud Shell") };
azureCloudShellProfile.SetCommandline(L"Azure");
azureCloudShellProfile.SetStartingDirectory(DEFAULT_STARTING_DIRECTORY);
azureCloudShellProfile.SetColorScheme({ L"Vintage" });
azureCloudShellProfile.SetAcrylicOpacity(0.6);
azureCloudShellProfile.SetUseAcrylic(true);
azureCloudShellProfile.SetCloseOnExit(false);
azureCloudShellProfile.SetConnectionType(AzureConnectionType);
profiles.emplace_back(azureCloudShellProfile);
}
return profiles;
}

View file

@ -0,0 +1,34 @@
/*++
Copyright (c) Microsoft Corporation
Licensed under the MIT license.
Module Name:
- AzureCloudShellGenerator
Abstract:
- This is the dynamic profile generator for the azure cloud shell connector.
Checks if the Azure Cloud shell is available on this platform, and if it is,
creates a profile to be able to launch it.
Author(s):
- Mike Griese - August 2019
--*/
#pragma once
#include "IDynamicProfileGenerator.h"
static constexpr GUID AzureConnectionType = { 0xd9fcfdfa, 0xa479, 0x412c, { 0x83, 0xb7, 0xc5, 0x64, 0xe, 0x61, 0xcd, 0x62 } };
namespace TerminalApp
{
class AzureCloudShellGenerator : public TerminalApp::IDynamicProfileGenerator
{
public:
AzureCloudShellGenerator() = default;
~AzureCloudShellGenerator() = default;
std::wstring_view GetNamespace() override;
std::vector<TerminalApp::Profile> GenerateProfiles() override;
};
};

View file

@ -11,107 +11,40 @@
#include "../../inc/DefaultSettings.h"
#include "Utils.h"
#include "PowershellCoreProfileGenerator.h"
#include "WslDistroGenerator.h"
#include "AzureCloudShellGenerator.h"
using namespace winrt::Microsoft::Terminal::Settings;
using namespace ::TerminalApp;
using namespace winrt::Microsoft::Terminal::TerminalControl;
using namespace winrt::TerminalApp;
using namespace Microsoft::Console;
// {2bde4a90-d05f-401c-9492-e40884ead1d8}
// uuidv5 properties: name format is UTF-16LE bytes
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}" };
CascadiaSettings::CascadiaSettings() :
_globals{},
_profiles{}
CascadiaSettings(true)
{
}
CascadiaSettings::~CascadiaSettings()
{
}
// Method Description:
// - Create a set of profiles to use as the "default" profiles when initializing
// the terminal. Currently, we create two or three profiles:
// * one for cmd.exe
// * one for powershell.exe (inbox Windows Powershell)
// * if Powershell Core (pwsh.exe) is installed, we'll create another for
// Powershell Core.
void CascadiaSettings::_CreateDefaultProfiles()
{
auto cmdProfile{ _CreateDefaultProfile(L"cmd") };
cmdProfile.SetFontFace(L"Consolas");
cmdProfile.SetCommandline(L"cmd.exe");
cmdProfile.SetStartingDirectory(DEFAULT_STARTING_DIRECTORY);
cmdProfile.SetColorScheme({ L"Campbell" });
cmdProfile.SetAcrylicOpacity(0.75);
cmdProfile.SetUseAcrylic(true);
auto powershellProfile{ _CreateDefaultProfile(L"Windows PowerShell") };
powershellProfile.SetCommandline(L"powershell.exe");
powershellProfile.SetStartingDirectory(DEFAULT_STARTING_DIRECTORY);
powershellProfile.SetColorScheme({ L"Campbell" });
powershellProfile.SetDefaultBackground(POWERSHELL_BLUE);
powershellProfile.SetUseAcrylic(false);
// If the user has installed PowerShell Core, we add PowerShell Core as a default.
// PowerShell Core default folder is "%PROGRAMFILES%\PowerShell\[Version]\".
std::filesystem::path psCoreCmdline{};
if (_isPowerShellCoreInstalled(psCoreCmdline))
{
auto pwshProfile{ _CreateDefaultProfile(L"PowerShell Core") };
pwshProfile.SetCommandline(std::move(psCoreCmdline));
pwshProfile.SetStartingDirectory(DEFAULT_STARTING_DIRECTORY);
pwshProfile.SetColorScheme({ L"Campbell" });
// If powershell core is installed, we'll use that as the default.
// Otherwise, we'll use normal Windows Powershell as the default.
_profiles.emplace_back(pwshProfile);
_globals.SetDefaultProfile(pwshProfile.GetGuid());
}
else
{
_globals.SetDefaultProfile(powershellProfile.GetGuid());
}
_profiles.emplace_back(powershellProfile);
_profiles.emplace_back(cmdProfile);
if (winrt::Microsoft::Terminal::TerminalConnection::AzureConnection::IsAzureConnectionAvailable())
{
auto azureCloudShellProfile{ _CreateDefaultProfile(L"Azure Cloud Shell") };
azureCloudShellProfile.SetCommandline(L"Azure");
azureCloudShellProfile.SetStartingDirectory(DEFAULT_STARTING_DIRECTORY);
azureCloudShellProfile.SetColorScheme({ L"Vintage" });
azureCloudShellProfile.SetAcrylicOpacity(0.6);
azureCloudShellProfile.SetUseAcrylic(true);
azureCloudShellProfile.SetCloseOnExit(false);
azureCloudShellProfile.SetConnectionType(AzureConnectionType);
_profiles.emplace_back(azureCloudShellProfile);
}
try
{
_AppendWslProfiles(_profiles);
}
CATCH_LOG()
}
// Method Description:
// - Initialize this object with default color schemes, profiles, and keybindings.
// Constructor Description:
// - Creates a new settings object. If addDynamicProfiles is true, we'll
// automatically add the built-in profile generators to our list of profile
// generators. Set this to `false` for unit testing.
// Arguments:
// - <none>
// Return Value:
// - <none>
void CascadiaSettings::CreateDefaults()
// - addDynamicProfiles: if true, we'll add the built-in DPGs.
CascadiaSettings::CascadiaSettings(const bool addDynamicProfiles)
{
_CreateDefaultProfiles();
if (addDynamicProfiles)
{
_profileGenerators.emplace_back(std::make_unique<PowershellCoreProfileGenerator>());
_profileGenerators.emplace_back(std::make_unique<WslDistroGenerator>());
_profileGenerators.emplace_back(std::make_unique<AzureCloudShellGenerator>());
}
}
// Method Description:
@ -196,160 +129,6 @@ GlobalAppSettings& CascadiaSettings::GlobalSettings()
return _globals;
}
// Function Description:
// - Returns true if the user has installed PowerShell Core. This will check
// both %ProgramFiles% and %ProgramFiles(x86)%, and will return true if
// powershell core was installed in either location.
// Arguments:
// - A ref of a path that receives the result of PowerShell Core pwsh.exe full path.
// Return Value:
// - true iff powershell core (pwsh.exe) is present.
bool CascadiaSettings::_isPowerShellCoreInstalled(std::filesystem::path& cmdline)
{
return _isPowerShellCoreInstalledInPath(L"%ProgramFiles%", cmdline) ||
_isPowerShellCoreInstalledInPath(L"%ProgramFiles(x86)%", cmdline);
}
// Function Description:
// - Returns true if the user has installed PowerShell Core.
// Arguments:
// - A string that contains an environment-variable string in the form: %variableName%.
// - A ref of a path that receives the result of PowerShell Core pwsh.exe full path.
// Return Value:
// - true iff powershell core (pwsh.exe) is present in the given path
bool CascadiaSettings::_isPowerShellCoreInstalledInPath(const std::wstring_view programFileEnv, std::filesystem::path& cmdline)
{
std::wstring programFileEnvNulTerm{ programFileEnv };
std::filesystem::path psCorePath{ wil::ExpandEnvironmentStringsW<std::wstring>(programFileEnvNulTerm.data()) };
psCorePath /= L"PowerShell";
if (std::filesystem::exists(psCorePath))
{
for (auto& p : std::filesystem::directory_iterator(psCorePath))
{
psCorePath = p.path();
psCorePath /= L"pwsh.exe";
if (std::filesystem::exists(psCorePath))
{
cmdline = psCorePath;
return true;
}
}
}
return false;
}
// Function Description:
// - Adds all of the WSL profiles to the provided container.
// Arguments:
// - A ref to the profiles container where the WSL profiles are to be added
// Return Value:
// - <none>
void CascadiaSettings::_AppendWslProfiles(std::vector<TerminalApp::Profile>& profileStorage)
{
wil::unique_handle readPipe;
wil::unique_handle writePipe;
SECURITY_ATTRIBUTES sa{ sizeof(sa), nullptr, true };
THROW_IF_WIN32_BOOL_FALSE(CreatePipe(&readPipe, &writePipe, &sa, 0));
STARTUPINFO si{};
si.cb = sizeof(si);
si.dwFlags = STARTF_USESTDHANDLES;
si.hStdOutput = writePipe.get();
si.hStdError = writePipe.get();
wil::unique_process_information pi{};
wil::unique_cotaskmem_string systemPath;
THROW_IF_FAILED(wil::GetSystemDirectoryW(systemPath));
std::wstring command(systemPath.get());
command += L"\\wsl.exe --list";
THROW_IF_WIN32_BOOL_FALSE(CreateProcessW(nullptr,
const_cast<LPWSTR>(command.c_str()),
nullptr,
nullptr,
TRUE,
CREATE_NO_WINDOW,
nullptr,
nullptr,
&si,
&pi));
switch (WaitForSingleObject(pi.hProcess, INFINITE))
{
case WAIT_OBJECT_0:
break;
case WAIT_ABANDONED:
case WAIT_TIMEOUT:
THROW_HR(ERROR_CHILD_NOT_COMPLETE);
case WAIT_FAILED:
THROW_LAST_ERROR();
default:
THROW_HR(ERROR_UNHANDLED_EXCEPTION);
}
DWORD exitCode;
if (GetExitCodeProcess(pi.hProcess, &exitCode) == false)
{
THROW_HR(E_INVALIDARG);
}
else if (exitCode != 0)
{
return;
}
DWORD bytesAvailable;
THROW_IF_WIN32_BOOL_FALSE(PeekNamedPipe(readPipe.get(), nullptr, NULL, nullptr, &bytesAvailable, nullptr));
std::wfstream pipe{ _wfdopen(_open_osfhandle((intptr_t)readPipe.get(), _O_WTEXT | _O_RDONLY), L"r") };
// don't worry about the handle returned from wfdOpen, readPipe handle is already managed by wil
// and closing the file handle will cause an error.
std::wstring wline;
std::getline(pipe, wline); // remove the header from the output.
while (pipe.tellp() < bytesAvailable)
{
std::getline(pipe, wline);
std::wstringstream wlinestream(wline);
if (wlinestream)
{
std::wstring distName;
std::getline(wlinestream, distName, L'\r');
size_t firstChar = distName.find_first_of(L"( ");
// Some localizations don't have a space between the name and "(Default)"
// https://github.com/microsoft/terminal/issues/1168#issuecomment-500187109
if (firstChar < distName.size())
{
distName.resize(firstChar);
}
auto WSLDistro{ _CreateDefaultProfile(distName) };
WSLDistro.SetCommandline(L"wsl.exe -d " + distName);
WSLDistro.SetColorScheme({ L"Campbell" });
WSLDistro.SetStartingDirectory(DEFAULT_STARTING_DIRECTORY);
std::wstring iconPath{ PACKAGED_PROFILE_ICON_PATH };
iconPath.append(DEFAULT_LINUX_ICON_GUID);
iconPath.append(PACKAGED_PROFILE_ICON_EXTENSION);
WSLDistro.SetIconPath(iconPath);
profileStorage.emplace_back(WSLDistro);
}
}
}
// Method Description:
// - Helper function for creating a skeleton default profile with a pre-populated
// guid and name.
// Arguments:
// - name: the name of the new profile.
// Return Value:
// - A Profile, ready to be filled in
Profile CascadiaSettings::_CreateDefaultProfile(const std::wstring_view name)
{
auto profileGuid{ Utils::CreateV5Uuid(TERMINAL_PROFILE_NAMESPACE_GUID, gsl::as_bytes(gsl::make_span(name))) };
Profile newProfile{ profileGuid };
newProfile.SetName(static_cast<std::wstring>(name));
std::wstring iconPath{ PACKAGED_PROFILE_ICON_PATH };
iconPath.append(Utils::GuidToString(profileGuid));
iconPath.append(PACKAGED_PROFILE_ICON_EXTENSION);
newProfile.SetIconPath(iconPath);
return newProfile;
}
// Method Description:
// - Gets our list of warnings we found during loading. These are things that we
// knew were bad when we called `_ValidateSettings` last.

View file

@ -20,8 +20,7 @@ Author(s):
#include "GlobalAppSettings.h"
#include "TerminalWarnings.h"
#include "Profile.h"
static constexpr GUID AzureConnectionType = { 0xd9fcfdfa, 0xa479, 0x412c, { 0x83, 0xb7, 0xc5, 0x64, 0xe, 0x61, 0xcd, 0x62 } };
#include "IDynamicProfileGenerator.h"
// fwdecl unittest classes
namespace TerminalAppLocalTests
@ -30,7 +29,11 @@ namespace TerminalAppLocalTests
class ProfileTests;
class ColorSchemeTests;
class KeyBindingsTests;
}
};
namespace TerminalAppUnitTests
{
class DynamicProfileTests;
};
namespace TerminalApp
{
@ -41,7 +44,7 @@ class TerminalApp::CascadiaSettings final
{
public:
CascadiaSettings();
~CascadiaSettings();
CascadiaSettings(const bool addDynamicProfiles);
static std::unique_ptr<CascadiaSettings> LoadDefaults();
static std::unique_ptr<CascadiaSettings> LoadAll();
@ -64,8 +67,6 @@ public:
const Profile* FindProfile(GUID profileGuid) const noexcept;
void CreateDefaults();
std::vector<TerminalApp::SettingsLoadWarnings>& GetWarnings();
private:
@ -73,17 +74,22 @@ private:
std::vector<Profile> _profiles;
std::vector<TerminalApp::SettingsLoadWarnings> _warnings;
std::vector<std::unique_ptr<TerminalApp::IDynamicProfileGenerator>> _profileGenerators;
std::string _userSettingsString;
Json::Value _userSettings;
Json::Value _defaultSettings;
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);
void _ParseJsonString(std::string_view fileData, const bool isDefaultSettings);
static const Json::Value& _GetProfilesJsonObject(const Json::Value& json);
static const Json::Value& _GetDisabledProfileSourcesJsonObject(const Json::Value& json);
bool _AppendDynamicProfilesToUserSettings();
void _LoadDynamicProfiles();
static bool _IsPackaged();
static void _WriteSettings(const std::string_view content);
@ -98,13 +104,9 @@ private:
void _ReorderProfilesToMatchUserSettingsOrder();
void _RemoveHiddenProfiles();
static bool _isPowerShellCoreInstalledInPath(const std::wstring_view programFileEnv, std::filesystem::path& cmdline);
static bool _isPowerShellCoreInstalled(std::filesystem::path& cmdline);
static void _AppendWslProfiles(std::vector<TerminalApp::Profile>& profileStorage);
static Profile _CreateDefaultProfile(const std::wstring_view name);
friend class TerminalAppLocalTests::SettingsTests;
friend class TerminalAppLocalTests::ProfileTests;
friend class TerminalAppLocalTests::ColorSchemeTests;
friend class TerminalAppLocalTests::KeyBindingsTests;
friend class TerminalAppUnitTests::DynamicProfileTests;
};

View file

@ -32,7 +32,10 @@ static constexpr std::string_view KeybindingsKey{ "keybindings" };
static constexpr std::string_view GlobalsKey{ "globals" };
static constexpr std::string_view SchemesKey{ "schemes" };
static constexpr std::string_view DisabledProfileSourcesKey{ "disabledProfileSources" };
static constexpr std::string_view Utf8Bom{ u8"\uFEFF" };
static constexpr std::string_view DefaultProfilesIndentation{ " " };
// Method Description:
// - Creates a CascadiaSettings from whatever's saved on disk, or instantiates
@ -41,6 +44,9 @@ static constexpr std::string_view Utf8Bom{ u8"\uFEFF" };
// 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
// - Also runs and dynamic profile generators. If any of those generators create
// new profiles, we'll write the user settings back to the file, with the new
// profiles inserted into their list of profiles.
// Return Value:
// - a unique_ptr containing a new CascadiaSettings object.
std::unique_ptr<CascadiaSettings> CascadiaSettings::LoadAll()
@ -49,18 +55,51 @@ std::unique_ptr<CascadiaSettings> CascadiaSettings::LoadAll()
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();
bool needToWriteFile = false;
if (fileHasData)
{
resultPtr->_LayerJsonString(fileData.value(), false);
resultPtr->_ParseJsonString(fileData.value(), false);
}
else
{
// We didn't find the user settings. We'll need to create a file
// to use as the user defaults.
_WriteSettings(UserSettingsJson);
// For now, just parse our user settings template as their user settings.
resultPtr->_ParseJsonString(UserSettingsJson, false);
needToWriteFile = true;
}
// Load profiles from dynamic profile generators. _userSettings should be
// created by now, because we're going to check in there for any generators
// that should be disabled.
resultPtr->_LoadDynamicProfiles();
// Apply the user's settings
resultPtr->LayerJson(resultPtr->_userSettings);
// After layering the user settings, check if there are any new profiles
// that need to be inserted into their user settings file.
needToWriteFile = resultPtr->_AppendDynamicProfilesToUserSettings() || needToWriteFile;
// TODO:GH#2721 If powershell core is installed, we need to set that to the
// default profile, but only when the settings file was newly created. We'll
// re-write the segment of the user settings for "default profile" to have
// the powershell core GUID instead.
// If we created the file, or found new dynamic profiles, write the user
// settings string back to the file.
if (needToWriteFile)
{
// If AppendDynamicProfilesToUserSettings (or the pwsh check above)
// changed the file, then our local settings JSON is no longer accurate.
// We should re-parse, but not re-layer
resultPtr->_ParseJsonString(resultPtr->_userSettingsString, false);
_WriteSettings(resultPtr->_userSettingsString);
}
// If this throws, the app will catch it and use the default settings
@ -83,18 +122,66 @@ std::unique_ptr<CascadiaSettings> CascadiaSettings::LoadDefaults()
// 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);
resultPtr->_ParseJsonString(DefaultJson, true);
resultPtr->LayerJson(resultPtr->_defaultSettings);
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.
// - Runs each of the configured dynamic profile generators (DPGs). Adds
// profiles from any DPGs that ran to the end of our list of profiles.
// - Uses the Json::Value _userSettings to check which DPGs should not be run.
// If the user settings has any namespaces in the "disabledProfileSources"
// property, we'll ensure that any DPGs with a matching namespace _don't_ run.
// Arguments:
// - <none>
// Return Value:
// - <none>
void CascadiaSettings::_LoadDynamicProfiles()
{
std::unordered_set<std::wstring> ignoredNamespaces;
const auto disabledProfileSources = CascadiaSettings::_GetDisabledProfileSourcesJsonObject(_userSettings);
if (disabledProfileSources.isArray())
{
for (const auto& ns : disabledProfileSources)
{
ignoredNamespaces.emplace(GetWstringFromJson(ns));
}
}
const GUID nullGuid{ 0 };
for (auto& generator : _profileGenerators)
{
const std::wstring generatorNamespace{ generator->GetNamespace() };
if (ignoredNamespaces.find(generatorNamespace) != ignoredNamespaces.end())
{
// namespace should be ignored
}
else
{
auto profiles = generator->GenerateProfiles();
for (auto& profile : profiles)
{
// If the profile did not have a GUID when it was generated,
// we'll synthesize a GUID for it in _ValidateProfilesHaveGuid
profile.SetSource(generatorNamespace);
_profiles.emplace_back(profile);
}
}
}
}
// Method Description:
// - Attempts to read the given data as a string of JSON and parse that JSON
// into a Json::Value.
// - 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.
// - Does _not_ apply the json onto our current settings. Callers should make
// sure to call LayerJson to ensure the settings are applied.
// Arguments:
// - fileData: the string to parse as JSON data
// - isDefaultSettings: if true, we should store the parsed JSON as our
@ -102,10 +189,11 @@ std::unique_ptr<CascadiaSettings> CascadiaSettings::LoadDefaults()
// settings.
// Return Value:
// - <none>
void CascadiaSettings::_LayerJsonString(std::string_view fileData, const bool isDefaultSettings)
void CascadiaSettings::_ParseJsonString(std::string_view fileData, const bool isDefaultSettings)
{
// Ignore UTF-8 BOM
auto actualDataStart = fileData.data();
const auto actualDataEnd = fileData.data() + fileData.size();
if (fileData.compare(0, Utf8Bom.size(), Utf8Bom) == 0)
{
actualDataStart += Utf8Bom.size();
@ -119,14 +207,20 @@ void CascadiaSettings::_LayerJsonString(std::string_view fileData, const bool is
// 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))
if (!reader->parse(actualDataStart, actualDataEnd, &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);
// If this is the user settings, also store away the original settings
// string. We'll need to keep it around so we can modify it without
// re-serializing their settings.
if (!isDefaultSettings)
{
_userSettingsString = fileData;
}
}
// Method Description:
@ -148,6 +242,101 @@ void CascadiaSettings::SaveAll() const
_WriteSettings(serializedString);
}
// Method Description:
// - Finds all the dynamic profiles we've generated that _don't_ exist in the
// user's settings. Generates a minimal blob of json for them, and inserts
// them into the user's settings at the end of the list of profiles.
// - Does not reformat the user's settings file.
// - Does not write the file! Only modifies in-place the _userSettingsString
// member. Callers should make sure to call
// _WriteSettings(_userSettingsString) to make sure to persist these changes!
// - Assumes that the `profiles` object is at an indentation of 4 spaces, and
// therefore each profile should be indented 8 spaces. If the user's settings
// have a different indentation, we'll still insert valid json, it'll just be
// indented incorrectly.
// Arguments:
// - <none>
// Return Value:
// - true iff we've made changes to the _userSettingsString that should be persisted.
bool CascadiaSettings::_AppendDynamicProfilesToUserSettings()
{
// - Find the set of profiles that weren't either in the default profiles or
// in the user profiles. TODO:GH#2723 Do this in not O(N^2)
// - For each of those profiles,
// * Diff them from the default profile
// * Serialize that diff
// * Insert that diff to the end of the list of profiles.
const Profile defaultProfile;
Json::StreamWriterBuilder wbuilder;
// Use 4 spaces to indent instead of \t
wbuilder.settings_["indentation"] = " ";
auto isInJsonObj = [](const auto& profile, const auto& json) {
for (auto profileJson : _GetProfilesJsonObject(json))
{
if (profileJson.isObject())
{
if (profile.ShouldBeLayered(profileJson))
{
return true;
}
}
}
return false;
};
// Get the index in the user settings string of the _last_ profile.
// We want to start inserting profiles immediately following the last profile.
const auto userProfilesObj = _GetProfilesJsonObject(_userSettings);
const auto numProfiles = userProfilesObj.size();
const auto lastProfile = userProfilesObj[numProfiles - 1];
size_t currentInsertIndex = lastProfile.getOffsetLimit();
bool changedFile = false;
for (const auto& profile : _profiles)
{
// Skip profiles that are in the user settings or the default settings.
if (isInJsonObj(profile, _userSettings) || isInJsonObj(profile, _defaultSettings))
{
continue;
}
// Generate a diff for the profile, that contains the minimal set of
// changes to re-create this profile.
const auto diff = profile.GenerateStub();
auto profileSerialization = Json::writeString(wbuilder, diff);
// Add 8 spaces to the start of each line
profileSerialization.insert(0, DefaultProfilesIndentation);
// Get the first newline
size_t pos = profileSerialization.find("\n");
// for each newline...
while (pos != std::string::npos)
{
// Insert 8 spaces immediately following the current newline
profileSerialization.insert(pos + 1, DefaultProfilesIndentation);
// Get the next newline
pos = profileSerialization.find("\n", pos + 9);
}
// Write a comma, newline to the file
changedFile = true;
_userSettingsString.insert(currentInsertIndex, ",");
currentInsertIndex++;
_userSettingsString.insert(currentInsertIndex, "\n");
currentInsertIndex++;
// Write the profile's serialization to the file
_userSettingsString.insert(currentInsertIndex, profileSerialization);
currentInsertIndex += profileSerialization.size();
}
return changedFile;
}
// Method Description:
// - Serialize this object to a JsonObject.
// Arguments:
@ -242,6 +431,10 @@ void CascadiaSettings::LayerJson(const Json::Value& json)
// - 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.
// - For profiles that were created from a dynamic profile source, they'll have
// both a guid and source guid that must both match. If a user profile with a
// source set does not find a matching profile at load time, the profile
// should be ignored.
// Arguments:
// - json: an object which may be a partial serialization of a Profile object.
// Return Value:
@ -256,8 +449,14 @@ void CascadiaSettings::_LayerOrCreateProfile(const Json::Value& profileJson)
}
else
{
auto profile = Profile::FromJson(profileJson);
_profiles.emplace_back(profile);
// If this JSON represents a dynamic profile, we _shouldn't_ create the
// profile here. We only want to create profiles for profiles without a
// `source`. Dynamic profiles _must_ be layered on an existing profile.
if (!Profile::IsDynamicProfileObject(profileJson))
{
auto profile = Profile::FromJson(profileJson);
_profiles.emplace_back(profile);
}
}
}
@ -549,3 +748,21 @@ const Json::Value& CascadiaSettings::_GetProfilesJsonObject(const Json::Value& j
{
return json[JsonKey(ProfilesKey)];
}
// Function Description:
// - Gets the object in the given JSON object under the "disabledProfileSources"
// key. Returns null if there's no "disabledProfileSources" key.
// Arguments:
// - json: the json object to get the disabled profile sources from.
// Return Value:
// - the Json::Value representing the `disabledProfileSources` property from the
// given object
const Json::Value& CascadiaSettings::_GetDisabledProfileSourcesJsonObject(const Json::Value& json)
{
// Check the globals first, then look in the root.
if (json.isMember(JsonKey(GlobalsKey)))
{
return json[JsonKey(GlobalsKey)][JsonKey(DisabledProfileSourcesKey)];
}
return json[JsonKey(DisabledProfileSourcesKey)];
}

View file

@ -0,0 +1,33 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "DefaultProfileUtils.h"
#include "../../types/inc/utils.hpp"
static constexpr std::wstring_view PACKAGED_PROFILE_ICON_PATH{ L"ms-appx:///ProfileIcons/" };
static constexpr std::wstring_view PACKAGED_PROFILE_ICON_EXTENSION{ L".png" };
// Method Description:
// - Helper function for creating a skeleton default profile with a pre-populated
// guid and name.
// Arguments:
// - name: the name of the new profile.
// Return Value:
// - A Profile, ready to be filled in
TerminalApp::Profile CreateDefaultProfile(const std::wstring_view name)
{
const auto profileGuid{ Microsoft::Console::Utils::CreateV5Uuid(TERMINAL_PROFILE_NAMESPACE_GUID,
gsl::as_bytes(gsl::make_span(name))) };
TerminalApp::Profile newProfile{ profileGuid };
newProfile.SetName(static_cast<std::wstring>(name));
std::wstring iconPath{ PACKAGED_PROFILE_ICON_PATH };
iconPath.append(Microsoft::Console::Utils::GuidToString(profileGuid));
iconPath.append(PACKAGED_PROFILE_ICON_EXTENSION);
newProfile.SetIconPath(iconPath);
return newProfile;
}

View file

@ -0,0 +1,23 @@
/*++
Copyright (c) Microsoft Corporation
Licensed under the MIT license.
Abstract:
- This header stores our default namespace guid. This is used in the creation of
default and in-box dynamic profiles. It also provides a helper function for
creating a "default" profile. Prior to GH#754, this was used to create the
cmd, powershell, wsl, pwsh, and azure profiles. Now, this helper is used for
any of the in-box dynamic profile generators.
Author(s):
- Mike Griese - August 2019
-- */
#pragma once
#include "Profile.h"
// {2bde4a90-d05f-401c-9492-e40884ead1d8}
// uuidv5 properties: name format is UTF-16LE bytes
static constexpr GUID TERMINAL_PROFILE_NAMESPACE_GUID = { 0x2bde4a90, 0xd05f, 0x401c, { 0x94, 0x92, 0xe4, 0x8, 0x84, 0xea, 0xd1, 0xd8 } };
TerminalApp::Profile CreateDefaultProfile(const std::wstring_view name);

View file

@ -0,0 +1,37 @@
/*++
Copyright (c) Microsoft Corporation
Licensed under the MIT license.
Module Name:
- IDynamicProfileGenerator
Abstract:
- The DynamicProfileGenerator interface. A dynamic profile generator is a object
that can synthesize a list of profiles based on some arbitrary, typically
external criteria. Profiles from dynamic sources are only available in the
user's profiles if the generator actually ran and created the profile.
- Each DPG must have a unique namespace to associate with itself. If the
namespace is not unique, the generator risks affecting profiles from
conflicting generators.
Author(s):
- Mike Griese - August 2019
--*/
#pragma once
#include "Profile.h"
namespace TerminalApp
{
class IDynamicProfileGenerator;
};
class TerminalApp::IDynamicProfileGenerator
{
public:
virtual ~IDynamicProfileGenerator() = 0;
virtual std::wstring_view GetNamespace() = 0;
virtual std::vector<TerminalApp::Profile> GenerateProfiles() = 0;
};
inline TerminalApp::IDynamicProfileGenerator::~IDynamicProfileGenerator() {}

View file

@ -0,0 +1,21 @@
/*++
Copyright (c) Microsoft Corporation
Licensed under the MIT license.
Abstract:
- This header simply contains all the namespaces of the "legacy" dynamic profile
generators. These generators were built-in to the code before we had a proper
concept of a dynamic profile source. As such, these profiles will exist in
user's settings without a `source` attribute, and we'll need to make sure to
handle layering them specially.
Author(s):
- Mike Griese - August 2019
--*/
#pragma once
static constexpr std::wstring_view WslGeneratorNamespace{ L"Windows.Terminal.Wsl" };
static constexpr std::wstring_view AzureGeneratorNamespace{ L"Windows.Terminal.Azure" };
static constexpr std::wstring_view PowershellCoreGeneratorNamespace{ L"Windows.Terminal.PowershellCore" };

View file

@ -0,0 +1,90 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "PowershellCoreProfileGenerator.h"
#include "LegacyProfileGeneratorNamespaces.h"
#include "../../types/inc/utils.hpp"
#include "../../inc/DefaultSettings.h"
#include "Utils.h"
#include "DefaultProfileUtils.h"
using namespace ::TerminalApp;
// Legacy GUIDs:
// - PowerShell Core 574e775e-4f2a-5b96-ac1e-a2962a402336
std::wstring_view PowershellCoreProfileGenerator::GetNamespace()
{
return PowershellCoreGeneratorNamespace;
}
// Method Description:
// - Checks if pwsh is installed, and if it is, creates a profile to launch it.
// Arguments:
// - <none>
// Return Value:
// - a vector with the PowerShell Core profile, if available.
std::vector<TerminalApp::Profile> PowershellCoreProfileGenerator::GenerateProfiles()
{
std::vector<TerminalApp::Profile> profiles;
std::filesystem::path psCoreCmdline;
if (_isPowerShellCoreInstalled(psCoreCmdline))
{
auto pwshProfile{ CreateDefaultProfile(L"PowerShell Core") };
pwshProfile.SetCommandline(std::move(psCoreCmdline));
pwshProfile.SetStartingDirectory(DEFAULT_STARTING_DIRECTORY);
pwshProfile.SetColorScheme({ L"Campbell" });
// If powershell core is installed, we'll use that as the default.
// Otherwise, we'll use normal Windows Powershell as the default.
profiles.emplace_back(pwshProfile);
}
return profiles;
}
// Function Description:
// - Returns true if the user has installed PowerShell Core. This will check
// both %ProgramFiles% and %ProgramFiles(x86)%, and will return true if
// powershell core was installed in either location.
// Arguments:
// - A ref of a path that receives the result of PowerShell Core pwsh.exe full path.
// Return Value:
// - true iff powershell core (pwsh.exe) is present.
bool PowershellCoreProfileGenerator::_isPowerShellCoreInstalled(std::filesystem::path& cmdline)
{
return _isPowerShellCoreInstalledInPath(L"%ProgramFiles%", cmdline) ||
_isPowerShellCoreInstalledInPath(L"%ProgramFiles(x86)%", cmdline);
}
// Function Description:
// - Returns true if the user has installed PowerShell Core.
// Arguments:
// - A string that contains an environment-variable string in the form: %variableName%.
// - A ref of a path that receives the result of PowerShell Core pwsh.exe full path.
// Return Value:
// - true iff powershell core (pwsh.exe) is present in the given path
bool PowershellCoreProfileGenerator::_isPowerShellCoreInstalledInPath(const std::wstring_view programFileEnv, std::filesystem::path& cmdline)
{
std::wstring programFileEnvNulTerm{ programFileEnv };
std::filesystem::path psCorePath{ wil::ExpandEnvironmentStringsW<std::wstring>(programFileEnvNulTerm.data()) };
psCorePath /= L"PowerShell";
if (std::filesystem::exists(psCorePath))
{
for (auto& p : std::filesystem::directory_iterator(psCorePath))
{
psCorePath = p.path();
psCorePath /= L"pwsh.exe";
if (std::filesystem::exists(psCorePath))
{
cmdline = psCorePath;
return true;
}
}
}
return false;
}

View file

@ -0,0 +1,36 @@
/*++
Copyright (c) Microsoft Corporation
Licensed under the MIT license.
Module Name:
- PowershellCoreProfileGenerator
Abstract:
- This is the dynamic profile generator for PowerShell Core. Checks if pwsh is
installed, and if it is, creates a profile to launch it.
Author(s):
- Mike Griese - August 2019
--*/
#pragma once
#include "IDynamicProfileGenerator.h"
namespace TerminalApp
{
class PowershellCoreProfileGenerator : public TerminalApp::IDynamicProfileGenerator
{
public:
PowershellCoreProfileGenerator() = default;
~PowershellCoreProfileGenerator() = default;
std::wstring_view GetNamespace() override;
std::vector<TerminalApp::Profile> GenerateProfiles() override;
private:
static bool _isPowerShellCoreInstalled(std::filesystem::path& cmdline);
static bool _isPowerShellCoreInstalledInPath(const std::wstring_view programFileEnv, std::filesystem::path& cmdline);
};
};

View file

@ -8,12 +8,16 @@
#include "../../types/inc/Utils.hpp"
#include <DefaultSettings.h>
#include "LegacyProfileGeneratorNamespaces.h"
using namespace TerminalApp;
using namespace winrt::Microsoft::Terminal::Settings;
using namespace winrt::Windows::UI::Xaml;
using namespace ::Microsoft::Console;
static constexpr std::string_view NameKey{ "name" };
static constexpr std::string_view GuidKey{ "guid" };
static constexpr std::string_view SourceKey{ "source" };
static constexpr std::string_view ColorSchemeKey{ "colorScheme" };
static constexpr std::string_view ColorSchemeKeyOld{ "colorscheme" };
static constexpr std::string_view HiddenKey{ "hidden" };
@ -122,6 +126,11 @@ GUID Profile::GetGuid() const noexcept
return _guid.value();
}
void Profile::SetSource(std::wstring_view sourceNamespace) noexcept
{
_source = sourceNamespace;
}
// Function Description:
// - Searches a list of color schemes to find one matching the given name. Will
//return the first match in the list, if the list has multiple schemes with the same name.
@ -227,8 +236,8 @@ TerminalSettings Profile::CreateTerminalSettings(const std::vector<ColorScheme>&
if (_backgroundImageAlignment)
{
const auto imageHorizontalAlignment = std::get<winrt::Windows::UI::Xaml::HorizontalAlignment>(_backgroundImageAlignment.value());
const auto imageVerticalAlignment = std::get<winrt::Windows::UI::Xaml::VerticalAlignment>(_backgroundImageAlignment.value());
const auto imageHorizontalAlignment = std::get<HorizontalAlignment>(_backgroundImageAlignment.value());
const auto imageVerticalAlignment = std::get<VerticalAlignment>(_backgroundImageAlignment.value());
terminalSettings.BackgroundImageHorizontalAlignment(imageHorizontalAlignment);
terminalSettings.BackgroundImageVerticalAlignment(imageVerticalAlignment);
}
@ -244,14 +253,9 @@ TerminalSettings Profile::CreateTerminalSettings(const std::vector<ColorScheme>&
// - a JsonObject which is an equivalent serialization of this object.
Json::Value Profile::ToJson() const
{
Json::Value root;
Json::Value root = GenerateStub();
///// Profile-specific settings /////
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 /////
@ -345,6 +349,98 @@ Json::Value Profile::ToJson() const
return root;
}
// Method Description:
// - This generates a json object `diff` s.t.
// this = other.LayerJson(diff)
// So if:
// - this has a nullopt for an optional, diff will have null for that member
// - this has a value for an optional, diff will have our value. If the other
// did _not_ have a value, and we did, diff will have our value.
// Arguments:
// - other: the other profile object to use as the "base" for this diff. The
// result could be layered upon that json object to re-create this object's
// serialization.
// Return Value:
// - a diff between this and the other object, such that this could be recreated
// from the diff and the other object.
Json::Value Profile::DiffToJson(const Profile& other) const
{
auto otherJson = other.ToJson();
auto myJson = ToJson();
Json::Value diff;
// Iterate in two steps:
// - first over all the keys in the 'other' object's serialization.
// - then over all the keys in our serialization.
// In this way, we ensure all keys from both objects are present in the
// final object.
for (const auto& key : otherJson.getMemberNames())
{
if (myJson.isMember(key))
{
// Both objects have the key
auto otherVal = otherJson[key];
auto myVal = myJson[key];
if (otherVal != myVal)
{
diff[key] = myVal;
}
}
else
{
// key is not in this json object. Set to null, so that when the
// diff is layered upon the original object, we'll properly set
// nullopt for any optionals that weren't present in this object.
diff[key] = Json::Value::null;
}
}
for (const auto& key : myJson.getMemberNames())
{
if (otherJson.isMember(key))
{
// both objects have this key. Do nothing, this is handled above
}
else
{
// We have a key the other object did not. Add our value.
diff[key] = myJson[key];
}
}
return diff;
}
// Method Description:
// - Generates a Json::Value which is a "stub" of this profile. This stub will
// have enough information that it could be layered with this profile.
// - This method is used during dynamic profile generation - if a profile is
// ever generated that didn't already exist in the user's settings, we'll add
// this stub to the user's settings file, so the user has an easy point to
// modify the generated profile.
// Arguments:
// - <none>
// Return Value:
// - A json::Value with a guid, name and source (if applicable).
Json::Value Profile::GenerateStub() const
{
Json::Value stub;
///// Profile-specific settings /////
if (_guid.has_value())
{
stub[JsonKey(GuidKey)] = winrt::to_string(Utils::GuidToString(_guid.value()));
}
stub[JsonKey(NameKey)] = winrt::to_string(_name);
if (_source.has_value())
{
stub[JsonKey(SourceKey)] = winrt::to_string(_source.value());
}
return stub;
}
// Method Description:
// - Create a new instance of this class from a serialized JsonObject.
// Arguments:
@ -375,16 +471,61 @@ bool Profile::ShouldBeLayered(const Json::Value& json) const
return false;
}
// First, check that GUIDs match. This is easy. If they don't match, they
// should _definitely_ not layer.
if (json.isMember(JsonKey(GuidKey)))
{
const auto guid{ json[JsonKey(GuidKey)] };
const auto otherGuid = Utils::GuidFromString(GetWstringFromJson(guid));
return _guid.value() == otherGuid;
if (_guid.value() != otherGuid)
{
return false;
}
}
else
{
// If the other json object didn't have a GUID, we definitely don't want
// to layer. We technically might have the same name, and would
// auto-generate the same guid, but they should be treated as different
// profiles.
return false;
}
// TODO: GH#754 - for profiles with a `source`, also check the `source` property.
const auto& otherSource = json.isMember(JsonKey(SourceKey)) ? json[JsonKey(SourceKey)] : Json::Value::null;
return false;
// For profiles with a `source`, also check the `source` property.
bool sourceMatches = false;
if (_source.has_value())
{
if (json.isMember(JsonKey(SourceKey)))
{
const auto otherSourceString = GetWstringFromJson(otherSource);
sourceMatches = otherSourceString == _source.value();
}
else
{
// Special case the legacy dynamic profiles here. In this case,
// `this` is a dynamic profile with a source, and our _source is one
// of the legacy DPG namespaces. We're looking to see if the other
// json object has the same guid, but _no_ "source"
if (_source.value() == WslGeneratorNamespace ||
_source.value() == AzureGeneratorNamespace ||
_source.value() == PowershellCoreGeneratorNamespace)
{
sourceMatches = true;
}
}
}
else
{
// We do not have a source. The only way we match is if source is set to null or "".
if (otherSource.isNull() || (otherSource.isString() && otherSource == ""))
{
sourceMatches = true;
}
}
return sourceMatches;
}
// Method Description:
@ -394,7 +535,7 @@ bool Profile::ShouldBeLayered(const Json::Value& json) const
// - 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)
Media::Stretch Profile::_ConvertJsonToStretchMode(const Json::Value& json)
{
return Profile::ParseImageStretchMode(json.asString());
}
@ -406,7 +547,7 @@ winrt::Windows::UI::Xaml::Media::Stretch Profile::_ConvertJsonToStretchMode(cons
// - 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)
std::tuple<HorizontalAlignment, VerticalAlignment> Profile::_ConvertJsonToAlignment(const Json::Value& json)
{
return Profile::ParseImageAlignment(json.asString());
}
@ -745,23 +886,23 @@ ScrollbarState Profile::ParseScrollbarState(const std::wstring& scrollbarState)
// - The value from the profiles.json file
// Return Value:
// - The corresponding enum value which maps to the string provided by the user
winrt::Windows::UI::Xaml::Media::Stretch Profile::ParseImageStretchMode(const std::string_view imageStretchMode)
Media::Stretch Profile::ParseImageStretchMode(const std::string_view imageStretchMode)
{
if (imageStretchMode == ImageStretchModeNone)
{
return winrt::Windows::UI::Xaml::Media::Stretch::None;
return Media::Stretch::None;
}
else if (imageStretchMode == ImageStretchModeFill)
{
return winrt::Windows::UI::Xaml::Media::Stretch::Fill;
return Media::Stretch::Fill;
}
else if (imageStretchMode == ImageStretchModeUniform)
{
return winrt::Windows::UI::Xaml::Media::Stretch::Uniform;
return Media::Stretch::Uniform;
}
else // Fall through to default behavior
{
return winrt::Windows::UI::Xaml::Media::Stretch::UniformToFill;
return Media::Stretch::UniformToFill;
}
}
@ -772,18 +913,18 @@ winrt::Windows::UI::Xaml::Media::Stretch Profile::ParseImageStretchMode(const st
// - imageStretchMode: The enum value to convert to a string.
// Return Value:
// - The string value for the given ImageStretchMode
std::string_view Profile::SerializeImageStretchMode(const winrt::Windows::UI::Xaml::Media::Stretch imageStretchMode)
std::string_view Profile::SerializeImageStretchMode(const Media::Stretch imageStretchMode)
{
switch (imageStretchMode)
{
case winrt::Windows::UI::Xaml::Media::Stretch::None:
case Media::Stretch::None:
return ImageStretchModeNone;
case winrt::Windows::UI::Xaml::Media::Stretch::Fill:
case Media::Stretch::Fill:
return ImageStretchModeFill;
case winrt::Windows::UI::Xaml::Media::Stretch::Uniform:
case Media::Stretch::Uniform:
return ImageStretchModeUniform;
default:
case winrt::Windows::UI::Xaml::Media::Stretch::UniformToFill:
case Media::Stretch::UniformToFill:
return ImageStretchModeUniformTofill;
}
}
@ -795,52 +936,52 @@ std::string_view Profile::SerializeImageStretchMode(const winrt::Windows::UI::Xa
// - The value from the profiles.json file
// Return Value:
// - The corresponding enum values tuple which maps to the string provided by the user
std::tuple<winrt::Windows::UI::Xaml::HorizontalAlignment, winrt::Windows::UI::Xaml::VerticalAlignment> Profile::ParseImageAlignment(const std::string_view imageAlignment)
std::tuple<HorizontalAlignment, VerticalAlignment> Profile::ParseImageAlignment(const std::string_view imageAlignment)
{
if (imageAlignment == ImageAlignmentTopLeft)
{
return std::make_tuple(winrt::Windows::UI::Xaml::HorizontalAlignment::Left,
winrt::Windows::UI::Xaml::VerticalAlignment::Top);
return std::make_tuple(HorizontalAlignment::Left,
VerticalAlignment::Top);
}
else if (imageAlignment == ImageAlignmentBottomLeft)
{
return std::make_tuple(winrt::Windows::UI::Xaml::HorizontalAlignment::Left,
winrt::Windows::UI::Xaml::VerticalAlignment::Bottom);
return std::make_tuple(HorizontalAlignment::Left,
VerticalAlignment::Bottom);
}
else if (imageAlignment == ImageAlignmentLeft)
{
return std::make_tuple(winrt::Windows::UI::Xaml::HorizontalAlignment::Left,
winrt::Windows::UI::Xaml::VerticalAlignment::Center);
return std::make_tuple(HorizontalAlignment::Left,
VerticalAlignment::Center);
}
else if (imageAlignment == ImageAlignmentTopRight)
{
return std::make_tuple(winrt::Windows::UI::Xaml::HorizontalAlignment::Right,
winrt::Windows::UI::Xaml::VerticalAlignment::Top);
return std::make_tuple(HorizontalAlignment::Right,
VerticalAlignment::Top);
}
else if (imageAlignment == ImageAlignmentBottomRight)
{
return std::make_tuple(winrt::Windows::UI::Xaml::HorizontalAlignment::Right,
winrt::Windows::UI::Xaml::VerticalAlignment::Bottom);
return std::make_tuple(HorizontalAlignment::Right,
VerticalAlignment::Bottom);
}
else if (imageAlignment == ImageAlignmentRight)
{
return std::make_tuple(winrt::Windows::UI::Xaml::HorizontalAlignment::Right,
winrt::Windows::UI::Xaml::VerticalAlignment::Center);
return std::make_tuple(HorizontalAlignment::Right,
VerticalAlignment::Center);
}
else if (imageAlignment == ImageAlignmentTop)
{
return std::make_tuple(winrt::Windows::UI::Xaml::HorizontalAlignment::Center,
winrt::Windows::UI::Xaml::VerticalAlignment::Top);
return std::make_tuple(HorizontalAlignment::Center,
VerticalAlignment::Top);
}
else if (imageAlignment == ImageAlignmentBottom)
{
return std::make_tuple(winrt::Windows::UI::Xaml::HorizontalAlignment::Center,
winrt::Windows::UI::Xaml::VerticalAlignment::Bottom);
return std::make_tuple(HorizontalAlignment::Center,
VerticalAlignment::Bottom);
}
else // Fall through to default alignment
{
return std::make_tuple(winrt::Windows::UI::Xaml::HorizontalAlignment::Center,
winrt::Windows::UI::Xaml::VerticalAlignment::Center);
return std::make_tuple(HorizontalAlignment::Center,
VerticalAlignment::Center);
}
}
@ -851,46 +992,46 @@ std::tuple<winrt::Windows::UI::Xaml::HorizontalAlignment, winrt::Windows::UI::Xa
// - imageAlignment: The enum values tuple to convert to a string.
// Return Value:
// - The string value for the given ImageAlignment
std::string_view Profile::SerializeImageAlignment(const std::tuple<winrt::Windows::UI::Xaml::HorizontalAlignment, winrt::Windows::UI::Xaml::VerticalAlignment> imageAlignment)
std::string_view Profile::SerializeImageAlignment(const std::tuple<HorizontalAlignment, VerticalAlignment> imageAlignment)
{
const auto imageHorizontalAlignment = std::get<winrt::Windows::UI::Xaml::HorizontalAlignment>(imageAlignment);
const auto imageVerticalAlignment = std::get<winrt::Windows::UI::Xaml::VerticalAlignment>(imageAlignment);
const auto imageHorizontalAlignment = std::get<HorizontalAlignment>(imageAlignment);
const auto imageVerticalAlignment = std::get<VerticalAlignment>(imageAlignment);
switch (imageHorizontalAlignment)
{
case winrt::Windows::UI::Xaml::HorizontalAlignment::Left:
case HorizontalAlignment::Left:
switch (imageVerticalAlignment)
{
case winrt::Windows::UI::Xaml::VerticalAlignment::Top:
case VerticalAlignment::Top:
return ImageAlignmentTopLeft;
case winrt::Windows::UI::Xaml::VerticalAlignment::Bottom:
case VerticalAlignment::Bottom:
return ImageAlignmentBottomLeft;
default:
case winrt::Windows::UI::Xaml::VerticalAlignment::Center:
case VerticalAlignment::Center:
return ImageAlignmentLeft;
}
case winrt::Windows::UI::Xaml::HorizontalAlignment::Right:
case HorizontalAlignment::Right:
switch (imageVerticalAlignment)
{
case winrt::Windows::UI::Xaml::VerticalAlignment::Top:
case VerticalAlignment::Top:
return ImageAlignmentTopRight;
case winrt::Windows::UI::Xaml::VerticalAlignment::Bottom:
case VerticalAlignment::Bottom:
return ImageAlignmentBottomRight;
default:
case winrt::Windows::UI::Xaml::VerticalAlignment::Center:
case VerticalAlignment::Center:
return ImageAlignmentRight;
}
default:
case winrt::Windows::UI::Xaml::HorizontalAlignment::Center:
case HorizontalAlignment::Center:
switch (imageVerticalAlignment)
{
case winrt::Windows::UI::Xaml::VerticalAlignment::Top:
case VerticalAlignment::Top:
return ImageAlignmentTop;
case winrt::Windows::UI::Xaml::VerticalAlignment::Bottom:
case VerticalAlignment::Bottom:
return ImageAlignmentBottom;
default:
case winrt::Windows::UI::Xaml::VerticalAlignment::Center:
case VerticalAlignment::Center:
return ImageAlignmentCenter;
}
}
@ -964,7 +1105,7 @@ void Profile::GenerateGuidIfNecessary() noexcept
{
// Always use the name to generate the temporary GUID. That way, across
// reloads, we'll generate the same static GUID.
_guid = Profile::_GenerateGuidForProfile(_name);
_guid = Profile::_GenerateGuidForProfile(_name, _source);
TraceLoggingWrite(
g_hTerminalAppProvider,
@ -975,15 +1116,38 @@ void Profile::GenerateGuidIfNecessary() noexcept
}
}
// Function Description:
// - Returns true if the given JSON object represents a dynamic profile object.
// If it is a dynamic profile object, we should make sure to only layer the
// object on a matching profile from a dynamic source.
// Arguments:
// - json: the partial serialization of a profile object to check
// Return Value:
// - true iff the object has a non-null `source` property
bool Profile::IsDynamicProfileObject(const Json::Value& json)
{
const auto& source = json.isMember(JsonKey(SourceKey)) ? json[JsonKey(SourceKey)] : Json::Value::null;
return !source.isNull();
}
// 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
GUID Profile::_GenerateGuidForProfile(const std::wstring& name, const std::optional<std::wstring>& source) noexcept
{
return Utils::CreateV5Uuid(RUNTIME_GENERATED_PROFILE_NAMESPACE_GUID, gsl::as_bytes(gsl::make_span(name)));
// If we have a _source, then we can from a dynamic profile generator. Use
// our source to build the naespace guid, instead of using the default GUID.
const GUID namespaceGuid = source.has_value() ?
Utils::CreateV5Uuid(RUNTIME_GENERATED_PROFILE_NAMESPACE_GUID, gsl::as_bytes(gsl::make_span(source.value()))) :
RUNTIME_GENERATED_PROFILE_NAMESPACE_GUID;
// Always use the name to generate the temporary GUID. That way, across
// reloads, we'll generate the same static GUID.
return Utils::CreateV5Uuid(namespaceGuid, gsl::as_bytes(gsl::make_span(name)));
}
// Function Description:
@ -1004,6 +1168,9 @@ GUID Profile::GetGuidOrGenerateForJson(const Json::Value& json) noexcept
return guid.value();
}
auto name = GetWstringFromJson(json[JsonKey(NameKey)]);
return Profile::_GenerateGuidForProfile(name);
const auto name = GetWstringFromJson(json[JsonKey(NameKey)]);
std::optional<std::wstring> source{ std::nullopt };
JsonUtils::GetOptionalString(json, SourceKey, source);
return Profile::_GenerateGuidForProfile(name, source);
}

View file

@ -25,6 +25,7 @@ namespace TerminalAppLocalTests
namespace TerminalAppUnitTests
{
class JsonTests;
class DynamicProfileTests;
};
// GUID used for generating GUIDs at runtime, for profiles that did not have a
@ -47,11 +48,15 @@ public:
winrt::Microsoft::Terminal::Settings::TerminalSettings CreateTerminalSettings(const std::vector<::TerminalApp::ColorScheme>& schemes) const;
Json::Value ToJson() const;
Json::Value DiffToJson(const Profile& other) const;
Json::Value GenerateStub() const;
static Profile FromJson(const Json::Value& json);
bool ShouldBeLayered(const Json::Value& json) const;
void LayerJson(const Json::Value& json);
static bool IsDynamicProfileObject(const Json::Value& json);
GUID GetGuid() const noexcept;
void SetSource(std::wstring_view sourceNamespace) noexcept;
std::wstring_view GetName() const noexcept;
bool HasConnectionType() const noexcept;
GUID GetConnectionType() const noexcept;
@ -77,6 +82,7 @@ public:
bool IsHidden() const noexcept;
void GenerateGuidIfNecessary() noexcept;
static GUID GetGuidOrGenerateForJson(const Json::Value& json) noexcept;
private:
@ -93,9 +99,10 @@ private:
static winrt::Microsoft::Terminal::Settings::CursorStyle _ParseCursorShape(const std::wstring& cursorShapeString);
static std::wstring_view _SerializeCursorStyle(const winrt::Microsoft::Terminal::Settings::CursorStyle cursorShape);
static GUID _GenerateGuidForProfile(const std::wstring& name) noexcept;
static GUID _GenerateGuidForProfile(const std::wstring& name, const std::optional<std::wstring>& source) noexcept;
std::optional<GUID> _guid{ std::nullopt };
std::optional<std::wstring> _source{ std::nullopt };
std::wstring _name;
std::optional<GUID> _connectionType;
bool _hidden;
@ -134,4 +141,5 @@ private:
friend class TerminalAppLocalTests::SettingsTests;
friend class TerminalAppLocalTests::ProfileTests;
friend class TerminalAppUnitTests::JsonTests;
friend class TerminalAppUnitTests::DynamicProfileTests;
};

View file

@ -8,6 +8,8 @@
#include "TerminalPage.g.cpp"
#include <winrt/Microsoft.UI.Xaml.XamlTypeInfo.h>
#include "AzureCloudShellGenerator.h" // For AzureConnectionType
using namespace winrt;
using namespace winrt::Windows::UI::Xaml;
using namespace winrt::Windows::UI::Core;

View file

@ -0,0 +1,118 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "WslDistroGenerator.h"
#include "LegacyProfileGeneratorNamespaces.h"
#include "../../types/inc/utils.hpp"
#include "../../inc/DefaultSettings.h"
#include "Utils.h"
#include <io.h>
#include <fcntl.h>
#include "DefaultProfileUtils.h"
using namespace ::TerminalApp;
// Legacy GUIDs:
// - Debian 58ad8b0c-3ef8-5f4d-bc6f-13e4c00f2530
// - Ubuntu 2c4de342-38b7-51cf-b940-2309a097f518
// - Alpine 1777cdf0-b2c4-5a63-a204-eb60f349ea7c
// - Ubuntu-18.04 c6eaf9f4-32a7-5fdc-b5cf-066e8a4b1e40
std::wstring_view WslDistroGenerator::GetNamespace()
{
return WslGeneratorNamespace;
}
// Method Description:
// - Enumerates all the installed WSL distros to create profiles for them.
// Arguments:
// - <none>
// Return Value:
// - a vector with all distros for all the installed WSL distros
std::vector<TerminalApp::Profile> WslDistroGenerator::GenerateProfiles()
{
std::vector<TerminalApp::Profile> profiles;
wil::unique_handle readPipe;
wil::unique_handle writePipe;
SECURITY_ATTRIBUTES sa{ sizeof(sa), nullptr, true };
THROW_IF_WIN32_BOOL_FALSE(CreatePipe(&readPipe, &writePipe, &sa, 0));
STARTUPINFO si{ 0 };
si.cb = sizeof(si);
si.dwFlags = STARTF_USESTDHANDLES;
si.hStdOutput = writePipe.get();
si.hStdError = writePipe.get();
wil::unique_process_information pi;
wil::unique_cotaskmem_string systemPath;
THROW_IF_FAILED(wil::GetSystemDirectoryW(systemPath));
std::wstring command(systemPath.get());
command += L"\\wsl.exe --list";
THROW_IF_WIN32_BOOL_FALSE(CreateProcessW(nullptr,
const_cast<LPWSTR>(command.c_str()),
nullptr,
nullptr,
TRUE,
CREATE_NO_WINDOW,
nullptr,
nullptr,
&si,
&pi));
switch (WaitForSingleObject(pi.hProcess, INFINITE))
{
case WAIT_OBJECT_0:
break;
case WAIT_ABANDONED:
case WAIT_TIMEOUT:
THROW_HR(ERROR_CHILD_NOT_COMPLETE);
case WAIT_FAILED:
THROW_LAST_ERROR();
default:
THROW_HR(ERROR_UNHANDLED_EXCEPTION);
}
DWORD exitCode;
if (GetExitCodeProcess(pi.hProcess, &exitCode) == false)
{
THROW_HR(E_INVALIDARG);
}
else if (exitCode != 0)
{
return profiles;
}
DWORD bytesAvailable;
THROW_IF_WIN32_BOOL_FALSE(PeekNamedPipe(readPipe.get(), nullptr, NULL, nullptr, &bytesAvailable, nullptr));
std::wfstream pipe{ _wfdopen(_open_osfhandle((intptr_t)readPipe.get(), _O_WTEXT | _O_RDONLY), L"r") };
// don't worry about the handle returned from wfdOpen, readPipe handle is already managed by wil
// and closing the file handle will cause an error.
std::wstring wline;
std::getline(pipe, wline); // remove the header from the output.
while (pipe.tellp() < bytesAvailable)
{
std::getline(pipe, wline);
std::wstringstream wlinestream(wline);
if (wlinestream)
{
std::wstring distName;
std::getline(wlinestream, distName, L'\r');
size_t firstChar = distName.find_first_of(L"( ");
// Some localizations don't have a space between the name and "(Default)"
// https://github.com/microsoft/terminal/issues/1168#issuecomment-500187109
if (firstChar < distName.size())
{
distName.resize(firstChar);
}
auto WSLDistro{ CreateDefaultProfile(distName) };
WSLDistro.SetCommandline(L"wsl.exe -d " + distName);
WSLDistro.SetColorScheme({ L"Campbell" });
WSLDistro.SetStartingDirectory(DEFAULT_STARTING_DIRECTORY);
WSLDistro.SetIconPath(L"ms-appx:///ProfileIcons/{9acb9455-ca41-5af7-950f-6bca1bc9722f}.png");
profiles.emplace_back(WSLDistro);
}
}
return profiles;
}

View file

@ -0,0 +1,30 @@
/*++
Copyright (c) Microsoft Corporation
Licensed under the MIT license.
Module Name:
- WslDistroGenerator
Abstract:
- This is the dynamic profile generator for WSL distros. Enumerates all the
installed WSL distros to create profiles for them.
Author(s):
- Mike Griese - August 2019
--*/
#pragma once
#include "IDynamicProfileGenerator.h"
namespace TerminalApp
{
class WslDistroGenerator : public TerminalApp::IDynamicProfileGenerator
{
public:
WslDistroGenerator() = default;
~WslDistroGenerator() = default;
std::wstring_view GetNamespace() override;
std::vector<TerminalApp::Profile> GenerateProfiles() override;
};
};

View file

@ -68,7 +68,12 @@
<ClInclude Include="../KeyChordSerialization.h" />
<ClInclude Include="../JsonUtils.h" />
<ClInclude Include="../Utils.h" />
<ClInclude Include="../DefaultProfileUtils.h" />
<ClInclude Include="../TerminalWarnings.h" />
<ClInclude Include="../IDynamicProfileGenerator.h" />
<ClInclude Include="../PowershellCoreProfileGenerator.h" />
<ClInclude Include="../WslDistroGenerator.h" />
<ClInclude Include="../AzureCloudShellGenerator.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="../ActionArgs.h" >
<DependentUpon>../ActionArgs.idl</DependentUpon>
@ -108,7 +113,11 @@
<ClCompile Include="../KeyChordSerialization.cpp" />
<ClCompile Include="../JsonUtils.cpp" />
<ClCompile Include="../Utils.cpp" />
<ClCompile Include="../DefaultProfileUtils.cpp" />
<ClCompile Include="../ScopedResourceLoader.cpp" />
<ClCompile Include="../PowershellCoreProfileGenerator.cpp" />
<ClCompile Include="../WslDistroGenerator.cpp" />
<ClCompile Include="../AzureCloudShellGenerator.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>

View file

@ -0,0 +1,677 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "../TerminalApp/ColorScheme.h"
#include "../TerminalApp/Profile.h"
#include "../TerminalApp/CascadiaSettings.h"
#include "../TerminalApp/LegacyProfileGeneratorNamespaces.h"
#include "../LocalTests_TerminalApp/JsonTestClass.h"
#include "TestDynamicProfileGenerator.h"
using namespace Microsoft::Console;
using namespace TerminalApp;
using namespace WEX::Logging;
using namespace WEX::TestExecution;
using namespace WEX::Common;
namespace TerminalAppUnitTests
{
class DynamicProfileTests : public JsonTestClass
{
BEGIN_TEST_CLASS(DynamicProfileTests)
TEST_CLASS_PROPERTY(L"ActivationContext", L"TerminalApp.Unit.Tests.manifest")
END_TEST_CLASS()
TEST_CLASS_SETUP(ClassSetup)
{
InitializeJsonReader();
return true;
}
TEST_METHOD(TestSimpleGenerate);
// Simple test of CascadiaSettings generating profiles with _LoadDynamicProfiles
TEST_METHOD(TestSimpleGenerateMultipleGenerators);
// Make sure we gen GUIDs for profiles without guids
TEST_METHOD(TestGenGuidsForProfiles);
// Profiles without a source should not be layered on those with one
TEST_METHOD(DontLayerUserProfilesOnDynamicProfiles);
TEST_METHOD(DoLayerUserProfilesOnDynamicsWhenSourceMatches);
// Make sure profiles that are disabled in _userSettings don't get generated
TEST_METHOD(TestDontRunDisabledGenerators);
// Make sure profiles that are disabled in _userSettings don't get generated
TEST_METHOD(TestLegacyProfilesMigrate);
// Both these do similar things:
// This makes sure that a profile with a `source` _only_ layers, it won't create a new profile
TEST_METHOD(UserProfilesWithInvalidSourcesAreIgnored);
// This does the same, but by disabling a profile source
TEST_METHOD(UserProfilesFromDisabledSourcesDontAppear);
};
void DynamicProfileTests::TestSimpleGenerate()
{
TestDynamicProfileGenerator gen{ L"Terminal.App.UnitTest" };
gen.pfnGenerate = []() {
std::vector<Profile> profiles;
Profile p0;
p0.SetName(L"profile0");
profiles.push_back(p0);
return profiles;
};
VERIFY_ARE_EQUAL(L"Terminal.App.UnitTest", gen.GetNamespace());
std::vector<Profile> profiles = gen.GenerateProfiles();
VERIFY_ARE_EQUAL(1u, profiles.size());
VERIFY_ARE_EQUAL(L"profile0", profiles.at(0).GetName());
VERIFY_IS_FALSE(profiles.at(0)._guid.has_value());
}
void DynamicProfileTests::TestSimpleGenerateMultipleGenerators()
{
auto gen0 = std::make_unique<TestDynamicProfileGenerator>(L"Terminal.App.UnitTest.0");
gen0->pfnGenerate = []() {
std::vector<Profile> profiles;
Profile p0;
p0.SetName(L"profile0");
profiles.push_back(p0);
return profiles;
};
auto gen1 = std::make_unique<TestDynamicProfileGenerator>(L"Terminal.App.UnitTest.1");
gen1->pfnGenerate = []() {
std::vector<Profile> profiles;
Profile p0;
p0.SetName(L"profile1");
profiles.push_back(p0);
return profiles;
};
CascadiaSettings settings{ false };
settings._profileGenerators.emplace_back(std::move(gen0));
settings._profileGenerators.emplace_back(std::move(gen1));
settings._LoadDynamicProfiles();
VERIFY_ARE_EQUAL(2u, settings._profiles.size());
VERIFY_ARE_EQUAL(L"profile0", settings._profiles.at(0).GetName());
VERIFY_IS_FALSE(settings._profiles.at(0)._guid.has_value());
VERIFY_ARE_EQUAL(L"profile1", settings._profiles.at(1).GetName());
VERIFY_IS_FALSE(settings._profiles.at(1)._guid.has_value());
}
void DynamicProfileTests::TestGenGuidsForProfiles()
{
// We'll generate GUIDs during
// CascadiaSettings::_ValidateProfilesHaveGuid. We should make sure that
// the GUID generated for a dynamic profile (with a source) is different
// than that of a profile without a source.
auto gen0 = std::make_unique<TestDynamicProfileGenerator>(L"Terminal.App.UnitTest.0");
gen0->pfnGenerate = []() {
std::vector<Profile> profiles;
Profile p0;
p0.SetName(L"profile0"); // this is _profiles.at(2)
profiles.push_back(p0);
return profiles;
};
auto gen1 = std::make_unique<TestDynamicProfileGenerator>(L"Terminal.App.UnitTest.1");
gen1->pfnGenerate = []() {
std::vector<Profile> profiles;
Profile p0, p1;
p0.SetName(L"profile0"); // this is _profiles.at(3)
p1.SetName(L"profile1"); // this is _profiles.at(4)
profiles.push_back(p0);
profiles.push_back(p1);
return profiles;
};
CascadiaSettings settings{ false };
settings._profileGenerators.emplace_back(std::move(gen0));
settings._profileGenerators.emplace_back(std::move(gen1));
Profile p0, p1;
p0.SetName(L"profile0"); // this is _profiles.at(0)
p1.SetName(L"profile1"); // this is _profiles.at(1)
settings._profiles.push_back(p0);
settings._profiles.push_back(p1);
settings._LoadDynamicProfiles();
VERIFY_ARE_EQUAL(5u, settings._profiles.size());
VERIFY_ARE_EQUAL(L"profile0", settings._profiles.at(0).GetName());
VERIFY_IS_FALSE(settings._profiles.at(0)._guid.has_value());
VERIFY_IS_FALSE(settings._profiles.at(0)._source.has_value());
VERIFY_ARE_EQUAL(L"profile1", settings._profiles.at(1).GetName());
VERIFY_IS_FALSE(settings._profiles.at(1)._guid.has_value());
VERIFY_IS_FALSE(settings._profiles.at(1)._source.has_value());
VERIFY_ARE_EQUAL(L"profile0", settings._profiles.at(2).GetName());
VERIFY_IS_FALSE(settings._profiles.at(2)._guid.has_value());
VERIFY_IS_TRUE(settings._profiles.at(2)._source.has_value());
VERIFY_ARE_EQUAL(L"profile0", settings._profiles.at(3).GetName());
VERIFY_IS_FALSE(settings._profiles.at(3)._guid.has_value());
VERIFY_IS_TRUE(settings._profiles.at(3)._source.has_value());
VERIFY_ARE_EQUAL(L"profile1", settings._profiles.at(4).GetName());
VERIFY_IS_FALSE(settings._profiles.at(4)._guid.has_value());
VERIFY_IS_TRUE(settings._profiles.at(4)._source.has_value());
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_ARE_NOT_EQUAL(settings._profiles.at(0)._guid.value(),
settings._profiles.at(1)._guid.value());
VERIFY_ARE_NOT_EQUAL(settings._profiles.at(0)._guid.value(),
settings._profiles.at(2)._guid.value());
VERIFY_ARE_NOT_EQUAL(settings._profiles.at(0)._guid.value(),
settings._profiles.at(3)._guid.value());
VERIFY_ARE_NOT_EQUAL(settings._profiles.at(1)._guid.value(),
settings._profiles.at(4)._guid.value());
VERIFY_ARE_NOT_EQUAL(settings._profiles.at(3)._guid.value(),
settings._profiles.at(4)._guid.value());
}
void DynamicProfileTests::DontLayerUserProfilesOnDynamicProfiles()
{
GUID guid0 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}");
GUID guid1 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-2222-49a3-80bd-e8fdd045185c}");
const std::string userProfiles{ R"(
{
"profiles": [
{
"name" : "profile0",
"guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}"
},
{
"name" : "profile1",
"guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}"
}
]
})" };
auto gen0 = std::make_unique<TestDynamicProfileGenerator>(L"Terminal.App.UnitTest.0");
gen0->pfnGenerate = [guid0, guid1]() {
std::vector<Profile> profiles;
Profile p0{ guid0 };
p0.SetName(L"profile0"); // this is _profiles.at(0)
profiles.push_back(p0);
return profiles;
};
auto gen1 = std::make_unique<TestDynamicProfileGenerator>(L"Terminal.App.UnitTest.1");
gen1->pfnGenerate = [guid0, guid1]() {
std::vector<Profile> profiles;
Profile p0{ guid0 }, p1{ guid1 };
p0.SetName(L"profile0"); // this is _profiles.at(1)
p1.SetName(L"profile1"); // this is _profiles.at(2)
profiles.push_back(p0);
profiles.push_back(p1);
return profiles;
};
CascadiaSettings settings{ false };
settings._profileGenerators.emplace_back(std::move(gen0));
settings._profileGenerators.emplace_back(std::move(gen1));
Log::Comment(NoThrowString().Format(
L"All profiles with the same name have the same GUID. However, they"
L" will not be layered, because they have different sources"));
// parse userProfiles as the user settings
settings._ParseJsonString(userProfiles, false);
VERIFY_ARE_EQUAL(0u, settings._profiles.size(), L"Just parsing the user settings doesn't actually layer them");
settings._LoadDynamicProfiles();
VERIFY_ARE_EQUAL(3u, settings._profiles.size());
settings.LayerJson(settings._userSettings);
VERIFY_ARE_EQUAL(5u, settings._profiles.size());
VERIFY_IS_TRUE(settings._profiles.at(0)._source.has_value());
VERIFY_IS_TRUE(settings._profiles.at(1)._source.has_value());
VERIFY_IS_TRUE(settings._profiles.at(2)._source.has_value());
VERIFY_IS_FALSE(settings._profiles.at(3)._source.has_value());
VERIFY_IS_FALSE(settings._profiles.at(4)._source.has_value());
VERIFY_ARE_EQUAL(L"Terminal.App.UnitTest.0", settings._profiles.at(0)._source.value());
VERIFY_ARE_EQUAL(L"Terminal.App.UnitTest.1", settings._profiles.at(1)._source.value());
VERIFY_ARE_EQUAL(L"Terminal.App.UnitTest.1", settings._profiles.at(2)._source.value());
VERIFY_IS_TRUE(settings._profiles.at(0)._guid.has_value());
VERIFY_IS_TRUE(settings._profiles.at(1)._guid.has_value());
VERIFY_IS_TRUE(settings._profiles.at(2)._guid.has_value());
VERIFY_IS_TRUE(settings._profiles.at(3)._guid.has_value());
VERIFY_IS_TRUE(settings._profiles.at(4)._guid.has_value());
VERIFY_ARE_EQUAL(guid0, settings._profiles.at(0)._guid.value());
VERIFY_ARE_EQUAL(guid0, settings._profiles.at(1)._guid.value());
VERIFY_ARE_EQUAL(guid1, settings._profiles.at(2)._guid.value());
VERIFY_ARE_EQUAL(guid0, settings._profiles.at(3)._guid.value());
VERIFY_ARE_EQUAL(guid1, settings._profiles.at(4)._guid.value());
}
void DynamicProfileTests::DoLayerUserProfilesOnDynamicsWhenSourceMatches()
{
GUID guid0 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}");
GUID guid1 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-2222-49a3-80bd-e8fdd045185c}");
const std::string userProfiles{ R"(
{
"profiles": [
{
"name" : "profile0FromUserSettings", // this is _profiles.at(0)
"guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}",
"source": "Terminal.App.UnitTest.0"
},
{
"name" : "profile1FromUserSettings", // this is _profiles.at(2)
"guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}",
"source": "Terminal.App.UnitTest.1"
}
]
})" };
auto gen0 = std::make_unique<TestDynamicProfileGenerator>(L"Terminal.App.UnitTest.0");
gen0->pfnGenerate = [guid0, guid1]() {
std::vector<Profile> profiles;
Profile p0{ guid0 };
p0.SetName(L"profile0"); // this is _profiles.at(0)
profiles.push_back(p0);
return profiles;
};
auto gen1 = std::make_unique<TestDynamicProfileGenerator>(L"Terminal.App.UnitTest.1");
gen1->pfnGenerate = [guid0, guid1]() {
std::vector<Profile> profiles;
Profile p0{ guid0 }, p1{ guid1 };
p0.SetName(L"profile0"); // this is _profiles.at(1)
p1.SetName(L"profile1"); // this is _profiles.at(2)
profiles.push_back(p0);
profiles.push_back(p1);
return profiles;
};
CascadiaSettings settings{ false };
settings._profileGenerators.emplace_back(std::move(gen0));
settings._profileGenerators.emplace_back(std::move(gen1));
Log::Comment(NoThrowString().Format(
L"All profiles with the same name have the same GUID. However, they"
L" will not be layered, because they have different source's"));
// parse userProfiles as the user settings
settings._ParseJsonString(userProfiles, false);
VERIFY_ARE_EQUAL(0u, settings._profiles.size(), L"Just parsing the user settings doesn't actually layer them");
settings._LoadDynamicProfiles();
VERIFY_ARE_EQUAL(3u, settings._profiles.size());
settings.LayerJson(settings._userSettings);
VERIFY_ARE_EQUAL(3u, settings._profiles.size());
VERIFY_IS_TRUE(settings._profiles.at(0)._source.has_value());
VERIFY_IS_TRUE(settings._profiles.at(1)._source.has_value());
VERIFY_IS_TRUE(settings._profiles.at(2)._source.has_value());
VERIFY_ARE_EQUAL(L"Terminal.App.UnitTest.0", settings._profiles.at(0)._source.value());
VERIFY_ARE_EQUAL(L"Terminal.App.UnitTest.1", settings._profiles.at(1)._source.value());
VERIFY_ARE_EQUAL(L"Terminal.App.UnitTest.1", settings._profiles.at(2)._source.value());
VERIFY_IS_TRUE(settings._profiles.at(0)._guid.has_value());
VERIFY_IS_TRUE(settings._profiles.at(1)._guid.has_value());
VERIFY_IS_TRUE(settings._profiles.at(2)._guid.has_value());
VERIFY_ARE_EQUAL(guid0, settings._profiles.at(0)._guid.value());
VERIFY_ARE_EQUAL(guid0, settings._profiles.at(1)._guid.value());
VERIFY_ARE_EQUAL(guid1, settings._profiles.at(2)._guid.value());
VERIFY_ARE_EQUAL(L"profile0FromUserSettings", settings._profiles.at(0)._name);
VERIFY_ARE_EQUAL(L"profile0", settings._profiles.at(1)._name);
VERIFY_ARE_EQUAL(L"profile1FromUserSettings", settings._profiles.at(2)._name);
}
void DynamicProfileTests::TestDontRunDisabledGenerators()
{
const std::string settings0String{ R"(
{
"disabledProfileSources": ["Terminal.App.UnitTest.0"]
})" };
const std::string settings1String{ R"(
{
"disabledProfileSources": ["Terminal.App.UnitTest.0", "Terminal.App.UnitTest.1"]
})" };
const auto settings0Json = VerifyParseSucceeded(settings0String);
auto gen0GenerateFn = []() {
std::vector<Profile> profiles;
Profile p0;
p0.SetName(L"profile0");
profiles.push_back(p0);
return profiles;
};
auto gen1GenerateFn = []() {
std::vector<Profile> profiles;
Profile p0, p1;
p0.SetName(L"profile1");
p1.SetName(L"profile2");
profiles.push_back(p0);
profiles.push_back(p1);
return profiles;
};
auto gen2GenerateFn = []() {
std::vector<Profile> profiles;
Profile p0, p1;
p0.SetName(L"profile3");
p1.SetName(L"profile4");
profiles.push_back(p0);
profiles.push_back(p1);
return profiles;
};
{
Log::Comment(NoThrowString().Format(
L"Case 1: Disable a single profile generator"));
CascadiaSettings settings{ false };
auto gen0 = std::make_unique<TestDynamicProfileGenerator>(L"Terminal.App.UnitTest.0");
auto gen1 = std::make_unique<TestDynamicProfileGenerator>(L"Terminal.App.UnitTest.1");
auto gen2 = std::make_unique<TestDynamicProfileGenerator>(L"Terminal.App.UnitTest.2");
gen0->pfnGenerate = gen0GenerateFn;
gen1->pfnGenerate = gen1GenerateFn;
gen2->pfnGenerate = gen2GenerateFn;
settings._profileGenerators.emplace_back(std::move(gen0));
settings._profileGenerators.emplace_back(std::move(gen1));
settings._profileGenerators.emplace_back(std::move(gen2));
// Parse as the user settings:
settings._ParseJsonString(settings0String, false);
settings._LoadDynamicProfiles();
VERIFY_ARE_EQUAL(4u, settings._profiles.size());
VERIFY_IS_TRUE(settings._profiles.at(0)._source.has_value());
VERIFY_IS_TRUE(settings._profiles.at(1)._source.has_value());
VERIFY_IS_TRUE(settings._profiles.at(2)._source.has_value());
VERIFY_IS_TRUE(settings._profiles.at(3)._source.has_value());
VERIFY_ARE_EQUAL(L"Terminal.App.UnitTest.1", settings._profiles.at(0)._source.value());
VERIFY_ARE_EQUAL(L"Terminal.App.UnitTest.1", settings._profiles.at(1)._source.value());
VERIFY_ARE_EQUAL(L"Terminal.App.UnitTest.2", settings._profiles.at(2)._source.value());
VERIFY_ARE_EQUAL(L"Terminal.App.UnitTest.2", settings._profiles.at(3)._source.value());
VERIFY_ARE_EQUAL(L"profile1", settings._profiles.at(0)._name);
VERIFY_ARE_EQUAL(L"profile2", settings._profiles.at(1)._name);
VERIFY_ARE_EQUAL(L"profile3", settings._profiles.at(2)._name);
VERIFY_ARE_EQUAL(L"profile4", settings._profiles.at(3)._name);
}
{
Log::Comment(NoThrowString().Format(
L"Case 2: Disable multiple profile generators"));
CascadiaSettings settings{ false };
auto gen0 = std::make_unique<TestDynamicProfileGenerator>(L"Terminal.App.UnitTest.0");
auto gen1 = std::make_unique<TestDynamicProfileGenerator>(L"Terminal.App.UnitTest.1");
auto gen2 = std::make_unique<TestDynamicProfileGenerator>(L"Terminal.App.UnitTest.2");
gen0->pfnGenerate = gen0GenerateFn;
gen1->pfnGenerate = gen1GenerateFn;
gen2->pfnGenerate = gen2GenerateFn;
settings._profileGenerators.emplace_back(std::move(gen0));
settings._profileGenerators.emplace_back(std::move(gen1));
settings._profileGenerators.emplace_back(std::move(gen2));
// Parse as the user settings:
settings._ParseJsonString(settings1String, false);
settings._LoadDynamicProfiles();
VERIFY_ARE_EQUAL(2u, settings._profiles.size());
VERIFY_IS_TRUE(settings._profiles.at(0)._source.has_value());
VERIFY_IS_TRUE(settings._profiles.at(1)._source.has_value());
VERIFY_ARE_EQUAL(L"Terminal.App.UnitTest.2", settings._profiles.at(0)._source.value());
VERIFY_ARE_EQUAL(L"Terminal.App.UnitTest.2", settings._profiles.at(1)._source.value());
VERIFY_ARE_EQUAL(L"profile3", settings._profiles.at(0)._name);
VERIFY_ARE_EQUAL(L"profile4", settings._profiles.at(1)._name);
}
}
void DynamicProfileTests::TestLegacyProfilesMigrate()
{
GUID guid0 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-0000-49a3-80bd-e8fdd045185c}");
GUID guid1 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}");
GUID guid2 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-2222-49a3-80bd-e8fdd045185c}");
GUID guid3 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-3333-49a3-80bd-e8fdd045185c}");
GUID guid4 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-4444-49a3-80bd-e8fdd045185c}");
const std::string settings0String{ R"(
{
"profiles": [
{
// This pwsh profile does not have a source, but should still be layered
"name" : "profile0FromUserSettings", // this is _profiles.at(0)
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}"
},
{
// This Azure profile does not have a source, but should still be layered
"name" : "profile3FromUserSettings", // this is _profiles.at(3)
"guid": "{6239a42c-3333-49a3-80bd-e8fdd045185c}"
},
{
// This profile did not come from a dynamic source
"name" : "profile4FromUserSettings", // this is _profiles.at(4)
"guid": "{6239a42c-4444-49a3-80bd-e8fdd045185c}"
},
{
// This WSL profile does not have a source, but should still be layered
"name" : "profile1FromUserSettings", // this is _profiles.at(1)
"guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}"
},
{
// This WSL profile does have a source, and should be layered
"name" : "profile2FromUserSettings", // this is _profiles.at(2)
"guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}",
"source": "Windows.Terminal.Wsl"
}
]
})" };
auto gen0 = std::make_unique<TestDynamicProfileGenerator>(L"Windows.Terminal.PowershellCore");
gen0->pfnGenerate = [guid0, guid1]() {
std::vector<Profile> profiles;
Profile p0{ guid0 };
p0.SetName(L"profile0");
profiles.push_back(p0);
return profiles;
};
auto gen1 = std::make_unique<TestDynamicProfileGenerator>(L"Windows.Terminal.Wsl");
gen1->pfnGenerate = [guid2, guid1]() {
std::vector<Profile> profiles;
Profile p0{ guid1 }, p1{ guid2 };
p0.SetName(L"profile1");
p1.SetName(L"profile2");
profiles.push_back(p0);
profiles.push_back(p1);
return profiles;
};
auto gen2 = std::make_unique<TestDynamicProfileGenerator>(L"Windows.Terminal.Azure");
gen2->pfnGenerate = [guid3]() {
std::vector<Profile> profiles;
Profile p0{ guid3 };
p0.SetName(L"profile3");
profiles.push_back(p0);
return profiles;
};
CascadiaSettings settings{ false };
settings._profileGenerators.emplace_back(std::move(gen0));
settings._profileGenerators.emplace_back(std::move(gen1));
settings._profileGenerators.emplace_back(std::move(gen2));
settings._ParseJsonString(settings0String, false);
VERIFY_ARE_EQUAL(0u, settings._profiles.size());
settings._LoadDynamicProfiles();
VERIFY_ARE_EQUAL(4u, settings._profiles.size());
VERIFY_IS_TRUE(settings._profiles.at(0)._source.has_value());
VERIFY_IS_TRUE(settings._profiles.at(1)._source.has_value());
VERIFY_IS_TRUE(settings._profiles.at(2)._source.has_value());
VERIFY_IS_TRUE(settings._profiles.at(3)._source.has_value());
VERIFY_ARE_EQUAL(L"Windows.Terminal.PowershellCore", settings._profiles.at(0)._source.value());
VERIFY_ARE_EQUAL(L"Windows.Terminal.Wsl", settings._profiles.at(1)._source.value());
VERIFY_ARE_EQUAL(L"Windows.Terminal.Wsl", settings._profiles.at(2)._source.value());
VERIFY_ARE_EQUAL(L"Windows.Terminal.Azure", settings._profiles.at(3)._source.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"profile2", settings._profiles.at(2)._name);
VERIFY_ARE_EQUAL(L"profile3", settings._profiles.at(3)._name);
settings.LayerJson(settings._userSettings);
VERIFY_ARE_EQUAL(5u, settings._profiles.size());
VERIFY_IS_TRUE(settings._profiles.at(0)._source.has_value());
VERIFY_IS_TRUE(settings._profiles.at(1)._source.has_value());
VERIFY_IS_TRUE(settings._profiles.at(2)._source.has_value());
VERIFY_IS_TRUE(settings._profiles.at(3)._source.has_value());
VERIFY_IS_FALSE(settings._profiles.at(4)._source.has_value());
VERIFY_ARE_EQUAL(L"Windows.Terminal.PowershellCore", settings._profiles.at(0)._source.value());
VERIFY_ARE_EQUAL(L"Windows.Terminal.Wsl", settings._profiles.at(1)._source.value());
VERIFY_ARE_EQUAL(L"Windows.Terminal.Wsl", settings._profiles.at(2)._source.value());
VERIFY_ARE_EQUAL(L"Windows.Terminal.Azure", settings._profiles.at(3)._source.value());
// settings._profiles.at(4) does not have a soruce
VERIFY_ARE_EQUAL(L"profile0FromUserSettings", settings._profiles.at(0)._name);
VERIFY_ARE_EQUAL(L"profile1FromUserSettings", settings._profiles.at(1)._name);
VERIFY_ARE_EQUAL(L"profile2FromUserSettings", settings._profiles.at(2)._name);
VERIFY_ARE_EQUAL(L"profile3FromUserSettings", settings._profiles.at(3)._name);
VERIFY_ARE_EQUAL(L"profile4FromUserSettings", settings._profiles.at(4)._name);
}
void DynamicProfileTests::UserProfilesWithInvalidSourcesAreIgnored()
{
GUID guid0 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}");
GUID guid1 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-2222-49a3-80bd-e8fdd045185c}");
const std::string settings0String{ R"(
{
"profiles": [
{
"name" : "profile0FromUserSettings", // this is _profiles.at(0)
"guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}",
"source": "Terminal.App.UnitTest.0"
},
{
"name" : "profile2", // this shouldn't be in the profiles at all
"guid": "{6239a42c-3333-49a3-80bd-e8fdd045185c}",
"source": "Terminal.App.UnitTest.1"
},
{
"name" : "profile3", // this is _profiles.at(3)
"guid": "{6239a42c-4444-49a3-80bd-e8fdd045185c}"
}
]
})" };
auto gen0 = std::make_unique<TestDynamicProfileGenerator>(L"Terminal.App.UnitTest.0");
gen0->pfnGenerate = [guid0, guid1]() {
std::vector<Profile> profiles;
Profile p0{ guid0 };
p0.SetName(L"profile0"); // this is _profiles.at(0)
profiles.push_back(p0);
return profiles;
};
auto gen1 = std::make_unique<TestDynamicProfileGenerator>(L"Terminal.App.UnitTest.1");
gen1->pfnGenerate = [guid0, guid1]() {
std::vector<Profile> profiles;
Profile p0{ guid0 }, p1{ guid1 };
p0.SetName(L"profile0"); // this is _profiles.at(1)
p1.SetName(L"profile1"); // this is _profiles.at(2)
profiles.push_back(p0);
profiles.push_back(p1);
return profiles;
};
CascadiaSettings settings{ false };
settings._profileGenerators.emplace_back(std::move(gen0));
settings._profileGenerators.emplace_back(std::move(gen1));
settings._ParseJsonString(settings0String, false);
VERIFY_ARE_EQUAL(0u, settings._profiles.size());
settings._LoadDynamicProfiles();
VERIFY_ARE_EQUAL(3u, settings._profiles.size());
settings.LayerJson(settings._userSettings);
VERIFY_ARE_EQUAL(4u, settings._profiles.size());
}
void DynamicProfileTests::UserProfilesFromDisabledSourcesDontAppear()
{
GUID guid0 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}");
GUID guid1 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-2222-49a3-80bd-e8fdd045185c}");
const std::string settings0String{ R"(
{
"disabledProfileSources": ["Terminal.App.UnitTest.1"],
"profiles": [
{
"name" : "profile0FromUserSettings", // this is _profiles.at(0)
"guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}",
"source": "Terminal.App.UnitTest.0"
},
{
"name" : "profile1FromUserSettings", // this shouldn't be in the profiles at all
"guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}",
"source": "Terminal.App.UnitTest.1"
},
{
"name" : "profile3", // this is _profiles.at(1)
"guid": "{6239a42c-4444-49a3-80bd-e8fdd045185c}"
}
]
})" };
auto gen0 = std::make_unique<TestDynamicProfileGenerator>(L"Terminal.App.UnitTest.0");
gen0->pfnGenerate = [guid0, guid1]() {
std::vector<Profile> profiles;
Profile p0{ guid0 };
p0.SetName(L"profile0"); // this is _profiles.at(0)
profiles.push_back(p0);
return profiles;
};
auto gen1 = std::make_unique<TestDynamicProfileGenerator>(L"Terminal.App.UnitTest.1");
gen1->pfnGenerate = [guid0, guid1]() {
std::vector<Profile> profiles;
Profile p0{ guid0 }, p1{ guid1 };
p0.SetName(L"profile0"); // this shouldn't be in the profiles at all
p1.SetName(L"profile1"); // this shouldn't be in the profiles at all
profiles.push_back(p0);
profiles.push_back(p1);
return profiles;
};
CascadiaSettings settings{ false };
settings._profileGenerators.emplace_back(std::move(gen0));
settings._profileGenerators.emplace_back(std::move(gen1));
settings._ParseJsonString(settings0String, false);
VERIFY_ARE_EQUAL(0u, settings._profiles.size());
settings._LoadDynamicProfiles();
VERIFY_ARE_EQUAL(1u, settings._profiles.size());
settings.LayerJson(settings._userSettings);
VERIFY_ARE_EQUAL(2u, settings._profiles.size());
}
};

View file

@ -10,6 +10,7 @@ using namespace Microsoft::Console;
using namespace TerminalApp;
using namespace WEX::Logging;
using namespace WEX::TestExecution;
using namespace WEX::Common;
namespace TerminalAppUnitTests
{
@ -22,10 +23,15 @@ namespace TerminalAppUnitTests
TEST_METHOD(ParseInvalidJson);
TEST_METHOD(ParseSimpleColorScheme);
TEST_METHOD(ProfileGeneratesGuid);
TEST_METHOD(DiffProfile);
TEST_METHOD(DiffProfileWithNull);
TEST_CLASS_SETUP(ClassSetup)
{
reader = std::unique_ptr<Json::CharReader>(Json::CharReaderBuilder::CharReaderBuilder().newCharReader());
// Use 4 spaces to indent instead of \t
_builder.settings_["indentation"] = " ";
return true;
}
@ -34,6 +40,7 @@ namespace TerminalAppUnitTests
private:
std::unique_ptr<Json::CharReader> reader;
Json::StreamWriterBuilder _builder;
};
Json::Value JsonTests::VerifyParseSucceeded(std::string content)
@ -156,4 +163,57 @@ namespace TerminalAppUnitTests
VERIFY_ARE_EQUAL(profile4.GetGuid(), cmdGuid);
}
void JsonTests::DiffProfile()
{
Profile profile0;
Profile profile1;
Log::Comment(NoThrowString().Format(
L"Both these profiles are the same, their diff should have _no_ values"));
auto diff = profile1.DiffToJson(profile0);
Log::Comment(NoThrowString().Format(L"diff:%hs", Json::writeString(_builder, diff).c_str()));
VERIFY_ARE_EQUAL(0u, diff.getMemberNames().size());
profile1._name = L"profile1";
diff = profile1.DiffToJson(profile0);
Log::Comment(NoThrowString().Format(L"diff:%hs", Json::writeString(_builder, diff).c_str()));
VERIFY_ARE_EQUAL(1u, diff.getMemberNames().size());
}
void JsonTests::DiffProfileWithNull()
{
Profile profile0;
Profile profile1;
profile0._icon = L"foo";
Log::Comment(NoThrowString().Format(
L"Case 1: Base object has an optional that the derived does not - diff will have null for that value"));
auto diff = profile1.DiffToJson(profile0);
Log::Comment(NoThrowString().Format(L"diff:%hs", Json::writeString(_builder, diff).c_str()));
VERIFY_ARE_EQUAL(1u, diff.getMemberNames().size());
VERIFY_IS_TRUE(diff.isMember("icon"));
VERIFY_IS_TRUE(diff["icon"].isNull());
Log::Comment(NoThrowString().Format(
L"Case 2: Add an optional to the derived object that's not present in the root."));
profile0._icon = std::nullopt;
profile1._icon = L"bar";
diff = profile1.DiffToJson(profile0);
Log::Comment(NoThrowString().Format(L"diff:%hs", Json::writeString(_builder, diff).c_str()));
VERIFY_ARE_EQUAL(1u, diff.getMemberNames().size());
VERIFY_IS_TRUE(diff.isMember("icon"));
VERIFY_IS_TRUE(diff["icon"].isString());
VERIFY_IS_TRUE("bar" == diff["icon"].asString());
}
}

View file

@ -11,6 +11,7 @@
<!-- ========================= Cpp Files ======================== -->
<ItemGroup>
<ClCompile Include="JsonTests.cpp" />
<ClCompile Include="DynamicProfileTests.cpp" />
<ClCompile Include="precomp.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>

View file

@ -0,0 +1,44 @@
/*++
Copyright (c) Microsoft Corporation
Licensed under the MIT license.
Module Name:
- TestDynamicProfileGenerator.hpp
Abstract:
- This is a helper class for writing tests using dynamic profiles. Lets you
easily set a arbitrary namespace and generation function for the profiles.
Author(s):
- Mike Griese - August 2019
--*/
#include "../TerminalApp/IDynamicProfileGenerator.h"
namespace TerminalAppUnitTests
{
class TestDynamicProfileGenerator;
};
class TerminalAppUnitTests::TestDynamicProfileGenerator final :
public TerminalApp::IDynamicProfileGenerator
{
public:
TestDynamicProfileGenerator(std::wstring_view ns) :
_namespace{ ns } {};
std::wstring_view GetNamespace() override { return _namespace; };
std::vector<TerminalApp::Profile> GenerateProfiles() override
{
if (pfnGenerate)
{
return pfnGenerate();
}
return std::vector<TerminalApp::Profile>{};
}
std::wstring _namespace;
std::function<std::vector<TerminalApp::Profile>()> pfnGenerate{ nullptr };
};

View file

@ -44,6 +44,7 @@
#include <filesystem>
#include <functional>
#include <set>
#include <unordered_set>
// WIL
#include <wil/Common.h>