terminal/src/cascadia/TerminalSettingsModel/PowershellCoreProfileGenerator.cpp
Carlos Zamora e332c67f51
Add an Extensions page to the Settings UI (#18559)
This pull request adds an Extensions page to the Settings UI, which lets
you enable/disable extensions and see how they affect your settings
(i.e. adding/modifying profiles and adding color schemes). This page is
specifically designed for fragment extensions and dynamic profile
generators, but can be expanded on in the future as we develop a more
advanced extensions model.

App extensions extract the name and icon from the extension package and
display it in the UI. Dynamic profile generators extract the name and
icon from the generator and display it in the UI. We prefer to use the
display name for breadcrumbs when possible.

A "NEW" badge was added to the Extensions page's `NavigationViewItem` to
highlight that it's new. It goes away once the user visits it.

## Detailed Description of the Pull Request / Additional comments
- Settings Model changes:
   - `FragmentSettings` represents a parsed json fragment extension.
- `FragmentProfileEntry` and `FragmentColorSchemeEntry` are used to
track profiles and color schemes added/modified
- `ExtensionPackage` bundles the `FragmentSettings` together. This is
how we represent multiple JSON files in one extension.
   - `IDynamicProfileGenerator` exposes a `DisplayName` and `Icon`
- `ExtensionPackage`s created from app extensions extract the
`DisplayName` and `Icon` from the extension
- `ApplicationState` is used to track which badges have been dismissed
and prevent them from appearing again
- a `std::unordered_set` is used to keep track of the dismissed badges,
but we only expose a get and append function via the IDL to interact
with it
- Editor changes - view models:
   - `ExtensionsViewModel` operates as the main view model for the page.
- `FragmentProfileViewModel` and `FragmentColorSchemeViewModel` are used
to reference specific components of fragments. They also provide support
for navigating to the linked profile or color scheme via the settings
UI!
- `ExtensionPackageViewModel` is a VM for a group of extensions exposed
by a single source. This is mainly needed because a single source can
have multiple JSON fragments in it. This is used for the navigators on
the main page. Can be extended to provide additional information (i.e.
package logo, package name, etc.)
- `CurrentExtensionPackage` is used to track which extension package is
currently in view, if applicable (similar to how the new tab menu page
works)
- Editor changes - views:
- `Extensions.xaml` uses _a lot_ of data templates. These are reused in
`ItemsControl`s to display extension components.
- `ExtensionPackageTemplateSelector` is used to display
`ExtensionPackage`s with metadata vs simple ones that just have a source
(i.e. Git)
- Added a `NewInfoBadge` style that is just an InfoBadge with "New" in
it instead of a number or an icon. Based on
https://github.com/microsoft/PowerToys/pull/36939
- The visibility is bound to a `get` call to the `ApplicationState`
conducted via the `ExtensionsPageViewModel`. The VM is also responsible
for updating the state.
- Lazy loading extension objects
- Since most instances of Terminal won't actually open the settings UI,
it doesn't make sense to create all the extension objects upon startup.
Instead, we defer creating those objects until the user actually
navigates to the Extensions page. This is most of the work that happened
in `CascadiaSettingsSerialization.cpp`. The `SettingsLoader` can be used
specifically to load and create the extension objects.

## Validation Steps
 Keyboard navigation feels right
 Screen reader reads all info on screen properly
 Accessibility Insights FastPass found no issues
 "Discard changes" retains subpage, but removes any changes
 Extensions page nav item displays a badge if page hasn't been visited
 The badge is dismissed when the user visits the page

## Follow-ups
- Streamline a process for adding extensions from the new page
- Long-term, we can reuse the InfoBadge system and make the following
minor changes:
- `SettingContainer`: display the badge and add logic to read/write
`ApplicationState` appropriately (similarly to above)
   - `XPageViewModel`: 
- count all the badges that will be displayed and expose/bind that to
`InfoBadge.Value`
- If a whole page is new, we can just style the badge using the
`NewInfoBadge` style
2025-05-28 14:03:02 -05:00

361 lines
14 KiB
C++

// 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 "DynamicProfileUtils.h"
// These four are headers we do not want proliferating, so they're not in the PCH.
#include <winrt/Windows.ApplicationModel.h>
#include <winrt/Windows.Management.Deployment.h>
#include <appmodel.h>
#include <shlobj.h>
#include <LibraryResources.h>
static constexpr std::wstring_view POWERSHELL_PFN{ L"Microsoft.PowerShell_8wekyb3d8bbwe" };
static constexpr std::wstring_view POWERSHELL_PREVIEW_PFN{ L"Microsoft.PowerShellPreview_8wekyb3d8bbwe" };
static constexpr std::wstring_view PWSH_EXE{ L"pwsh.exe" };
static constexpr std::wstring_view POWERSHELL_ICON{ L"ms-appx:///ProfileIcons/pwsh.png" };
static constexpr std::wstring_view POWERSHELL_PREVIEW_ICON{ L"ms-appx:///ProfileIcons/pwsh-preview.png" };
static constexpr std::wstring_view GENERATOR_POWERSHELL_ICON{ L"ms-appx:///ProfileGeneratorIcons/PowerShell.png" };
static constexpr std::wstring_view POWERSHELL_PREFERRED_PROFILE_NAME{ L"PowerShell" };
namespace
{
enum PowerShellFlags
{
None = 0,
// These flags are used as a sort key, so they encode some native ordering.
// They are ordered such that the "most important" flags have the largest
// impact on the sort space. For example, since we want Preview to be very polar
// we give it the highest flag value.
// The "ideal" powershell instance has 0 flags (stable, native, Program Files location)
//
// With this ordering, the sort space ends up being (for PowerShell 6)
// (numerically greater values are on the left; this is flipped in the final sort)
//
// <-- Less Valued .................................... More Valued -->
// | All instances of PS 6 | All PS7 |
// | Preview | Stable | ~~~ |
// | Non-Native | Native | Non-Native | Native | ~~~ |
// | Trd | Pack | Trd | Pack | Trd | Pack | Trd | Pack | ~~~ |
// (where Pack is a stand-in for store, scoop, dotnet, though they have their own orders,
// and Trd is a stand-in for "Traditional" (Program Files))
//
// In short, flags with larger magnitudes are pushed further down (therefore valued less)
// distribution method (choose one)
Store = 1 << 0, // distributed via the store
Scoop = 1 << 1, // installed via Scoop
Dotnet = 1 << 2, // installed as a dotnet global tool
Traditional = 1 << 3, // installed in traditional Program Files locations
// native architecture (choose one)
WOWARM = 1 << 4, // non-native (Windows-on-Windows, ARM variety)
WOWx86 = 1 << 5, // non-native (Windows-on-Windows, x86 variety)
// build type (choose one)
Preview = 1 << 6, // preview version
};
DEFINE_ENUM_FLAG_OPERATORS(PowerShellFlags);
struct PowerShellInstance
{
int majorVersion; // 0 = we don't know, sort last.
PowerShellFlags flags;
std::filesystem::path executablePath;
constexpr bool operator<(const PowerShellInstance& second) const
{
if (majorVersion != second.majorVersion)
{
return majorVersion < second.majorVersion;
}
if (flags != second.flags)
{
return flags > second.flags; // flags are inverted because "0" is ideal; see above
}
return executablePath < second.executablePath; // fall back to path sorting
}
// Method Description:
// - Generates a name, based on flags, for a powershell instance.
// Return value:
// - the name
std::wstring Name() const
{
std::wstringstream namestream;
namestream << L"PowerShell";
if (WI_IsFlagSet(flags, PowerShellFlags::Store))
{
if (WI_IsFlagSet(flags, PowerShellFlags::Preview))
{
namestream << L" Preview";
}
namestream << L" (msix)";
}
else if (WI_IsFlagSet(flags, PowerShellFlags::Dotnet))
{
namestream << L" (dotnet global)";
}
else if (WI_IsFlagSet(flags, PowerShellFlags::Scoop))
{
namestream << L" (scoop)";
}
else
{
if (majorVersion < 7)
{
namestream << L" Core";
}
if (majorVersion != 0)
{
namestream << L" " << majorVersion;
}
if (WI_IsFlagSet(flags, PowerShellFlags::Preview))
{
namestream << L" Preview";
}
if (WI_IsFlagSet(flags, PowerShellFlags::WOWx86))
{
namestream << L" (x86)";
}
if (WI_IsFlagSet(flags, PowerShellFlags::WOWARM))
{
namestream << L" (ARM)";
}
}
return namestream.str();
}
};
}
using namespace ::Microsoft::Terminal::Settings::Model;
using namespace winrt::Microsoft::Terminal::Settings::Model;
// Function Description:
// - Finds all powershell instances with the traditional layout under a directory.
// - The "traditional" directory layout requires that pwsh.exe exist in a versioned directory, as in
// ROOT\6\pwsh.exe
// Arguments:
// - directory: the directory under which to search
// - flags: flags to apply to all found instances
// - out: the list into which to accumulate these instances.
static void _accumulateTraditionalLayoutPowerShellInstancesInDirectory(std::wstring_view directory, PowerShellFlags flags, std::vector<PowerShellInstance>& out)
{
const std::filesystem::path root{ wil::ExpandEnvironmentStringsW<std::wstring>(directory.data()) };
if (std::filesystem::exists(root))
{
for (const auto& versionedDir : std::filesystem::directory_iterator(root))
{
const auto versionedPath = versionedDir.path();
const auto executable = versionedPath / PWSH_EXE;
if (std::filesystem::exists(executable))
{
const auto preview = versionedPath.filename().native().find(L"-preview") != std::wstring::npos;
const auto previewFlag = preview ? PowerShellFlags::Preview : PowerShellFlags::None;
out.emplace_back(PowerShellInstance{ std::stoi(versionedPath.filename()),
PowerShellFlags::Traditional | flags | previewFlag,
executable });
}
}
}
}
// Function Description:
// - Finds the store package, if one exists, for a given package family name
// Arguments:
// - packageFamilyName: the package family name
// Return Value:
// - a package, or nullptr.
static winrt::Windows::ApplicationModel::Package _getStorePackage(const std::wstring_view packageFamilyName) noexcept
try
{
winrt::Windows::Management::Deployment::PackageManager packageManager;
auto foundPackages = packageManager.FindPackagesForUser(L"", packageFamilyName);
auto iterator = foundPackages.First();
if (!iterator.HasCurrent())
{
return nullptr;
}
return iterator.Current();
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
return nullptr;
}
// Function Description:
// - Finds all powershell instances that have App Execution Aliases in the standard location
// Arguments:
// - out: the list into which to accumulate these instances.
static void _accumulateStorePowerShellInstances(std::vector<PowerShellInstance>& out)
{
wil::unique_cotaskmem_string localAppDataFolder;
if (FAILED(SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &localAppDataFolder)))
{
return;
}
std::filesystem::path appExecAliasPath{ localAppDataFolder.get() };
appExecAliasPath /= L"Microsoft";
appExecAliasPath /= L"WindowsApps";
if (std::filesystem::exists(appExecAliasPath))
{
// App execution aliases for preview powershell
const auto previewPath = appExecAliasPath / POWERSHELL_PREVIEW_PFN;
if (std::filesystem::exists(previewPath))
{
const auto previewPackage = _getStorePackage(POWERSHELL_PREVIEW_PFN);
if (previewPackage)
{
out.emplace_back(PowerShellInstance{
gsl::narrow_cast<int>(previewPackage.Id().Version().Major),
PowerShellFlags::Store | PowerShellFlags::Preview,
previewPath / PWSH_EXE });
}
}
// App execution aliases for stable powershell
const auto gaPath = appExecAliasPath / POWERSHELL_PFN;
if (std::filesystem::exists(gaPath))
{
const auto gaPackage = _getStorePackage(POWERSHELL_PFN);
if (gaPackage)
{
out.emplace_back(PowerShellInstance{
gaPackage.Id().Version().Major,
PowerShellFlags::Store,
gaPath / PWSH_EXE,
});
}
}
}
}
// Function Description:
// - Finds a powershell instance that's just a pwsh.exe in a folder.
// - This function cannot determine the version number of such a powershell instance.
// Arguments:
// - directory: the directory under which to search
// - flags: flags to apply to all found instances
// - out: the list into which to accumulate these instances.
static void _accumulatePwshExeInDirectory(const std::wstring_view directory, const PowerShellFlags flags, std::vector<PowerShellInstance>& out)
{
const std::filesystem::path root{ wil::ExpandEnvironmentStringsW<std::wstring>(directory.data()) };
const auto pwshPath = root / PWSH_EXE;
if (std::filesystem::exists(pwshPath))
{
out.emplace_back(PowerShellInstance{ 0 /* we can't tell */, flags, pwshPath });
}
}
// Function Description:
// - Builds a comprehensive priority-ordered list of powershell instances.
// Return value:
// - a comprehensive priority-ordered list of powershell instances.
static std::vector<PowerShellInstance> _collectPowerShellInstances()
{
std::vector<PowerShellInstance> versions;
_accumulateTraditionalLayoutPowerShellInstancesInDirectory(L"%ProgramFiles%\\PowerShell", PowerShellFlags::None, versions);
#if defined(_M_AMD64) || defined(_M_ARM64) // No point in looking for WOW if we're not somewhere it exists
_accumulateTraditionalLayoutPowerShellInstancesInDirectory(L"%ProgramFiles(x86)%\\PowerShell", PowerShellFlags::WOWx86, versions);
#endif
#if defined(_M_ARM64) // no point in looking for WOA if we're not on ARM64
_accumulateTraditionalLayoutPowerShellInstancesInDirectory(L"%ProgramFiles(Arm)%\\PowerShell", PowerShellFlags::WOWARM, versions);
#endif
_accumulateStorePowerShellInstances(versions);
_accumulatePwshExeInDirectory(L"%USERPROFILE%\\.dotnet\\tools", PowerShellFlags::Dotnet, versions);
_accumulatePwshExeInDirectory(L"%USERPROFILE%\\scoop\\shims", PowerShellFlags::Scoop, versions);
std::sort(versions.rbegin(), versions.rend()); // sort in reverse (best first)
return versions;
}
// Legacy GUIDs:
// - PowerShell Core 574e775e-4f2a-5b96-ac1e-a2962a402336
static constexpr winrt::guid PowershellCoreGuid{ 0x574e775e, 0x4f2a, 0x5b96, { 0xac, 0x1e, 0xa2, 0x96, 0x2a, 0x40, 0x23, 0x36 } };
std::wstring_view PowershellCoreProfileGenerator::GetNamespace() const noexcept
{
return PowershellCoreGeneratorNamespace;
}
std::wstring_view PowershellCoreProfileGenerator::GetDisplayName() const noexcept
{
return RS_(L"PowershellCoreProfileGeneratorDisplayName");
}
std::wstring_view PowershellCoreProfileGenerator::GetIcon() const noexcept
{
return GENERATOR_POWERSHELL_ICON;
}
// 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.
void PowershellCoreProfileGenerator::GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles) const
{
const auto psInstances = _collectPowerShellInstances();
auto first = true;
for (const auto& psI : psInstances)
{
const auto name = psI.Name();
auto profile{ CreateDynamicProfile(name) };
const auto& unquotedCommandline = psI.executablePath.native();
std::wstring quotedCommandline;
quotedCommandline.reserve(unquotedCommandline.size() + 2);
quotedCommandline.push_back(L'"');
quotedCommandline.append(unquotedCommandline);
quotedCommandline.push_back(L'"');
profile->Commandline(winrt::hstring{ quotedCommandline });
profile->StartingDirectory(winrt::hstring{ DEFAULT_STARTING_DIRECTORY });
profile->DefaultAppearance().DarkColorSchemeName(L"Campbell");
profile->DefaultAppearance().LightColorSchemeName(L"Campbell");
profile->Icon(winrt::hstring{ WI_IsFlagSet(psI.flags, PowerShellFlags::Preview) ? POWERSHELL_PREVIEW_ICON : POWERSHELL_ICON });
if (first)
{
// Give the first ("algorithmically best") profile the official, and original, "PowerShell Core" GUID.
// This will turn the anchored default profile into "PowerShell Core Latest for Native Architecture through Store"
// (or the closest approximation thereof). It may choose a preview instance as the "best" if it is a higher version.
profile->Guid(PowershellCoreGuid);
profile->Name(winrt::hstring{ POWERSHELL_PREFERRED_PROFILE_NAME });
first = false;
}
profiles.emplace_back(std::move(profile));
}
}
// Function Description:
// - Returns the thing it's named for.
// Return value:
// - the thing it says in the name
const std::wstring_view PowershellCoreProfileGenerator::GetPreferredPowershellProfileName()
{
return POWERSHELL_PREFERRED_PROFILE_NAME;
}