diff --git a/src/cascadia/CascadiaPackage/ProfileIcons/Powershell_black_64.png b/src/cascadia/CascadiaPackage/ProfileIcons/Powershell_black_64.png new file mode 100644 index 00000000000..53bbbee10b7 Binary files /dev/null and b/src/cascadia/CascadiaPackage/ProfileIcons/Powershell_black_64.png differ diff --git a/src/cascadia/TerminalApp/TabManagement.cpp b/src/cascadia/TerminalApp/TabManagement.cpp index 3b2691df936..150c049e6bd 100644 --- a/src/cascadia/TerminalApp/TabManagement.cpp +++ b/src/cascadia/TerminalApp/TabManagement.cpp @@ -77,6 +77,16 @@ namespace winrt::TerminalApp::implementation } const auto settings{ TerminalSettings::CreateWithNewTerminalArgs(_settings, newTerminalArgs, *_bindings) }; + if (profile.Source() == L"Windows.Terminal.InstallPowerShell") + { + TraceLoggingWrite( + g_hTerminalAppProvider, + "InstallPowerShellStubInvoked", + TraceLoggingDescription("Event emitted when the 'Install Latest PowerShell' stub was invoked"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); + } + // Try to handle auto-elevation if (_maybeElevate(newTerminalArgs, settings, profile)) { diff --git a/src/cascadia/TerminalSettingsEditor/Extensions.xaml b/src/cascadia/TerminalSettingsEditor/Extensions.xaml index b7a1296f567..3f5241e1ec9 100644 --- a/src/cascadia/TerminalSettingsEditor/Extensions.xaml +++ b/src/cascadia/TerminalSettingsEditor/Extensions.xaml @@ -156,7 +156,8 @@ x:Uid="Extensions_NavigateToProfileButton" Click="NavigateToProfile_Click" Style="{StaticResource SettingContainerResetButtonStyle}" - Tag="{x:Bind Profile.Guid}"> + Tag="{x:Bind Profile.Guid}" + Visibility="{x:Bind mtu:Converters.InvertedBooleanToVisibility(Profile.Deleted)}"> diff --git a/src/cascadia/TerminalSettingsModel/AzureCloudShellGenerator.cpp b/src/cascadia/TerminalSettingsModel/AzureCloudShellGenerator.cpp index 9401c0137d3..65595f787f8 100644 --- a/src/cascadia/TerminalSettingsModel/AzureCloudShellGenerator.cpp +++ b/src/cascadia/TerminalSettingsModel/AzureCloudShellGenerator.cpp @@ -31,7 +31,7 @@ std::wstring_view AzureCloudShellGenerator::GetDisplayName() const noexcept // - // Return Value: // - a vector with the Azure Cloud Shell connection profile, if available. -void AzureCloudShellGenerator::GenerateProfiles(std::vector>& profiles) const +void AzureCloudShellGenerator::GenerateProfiles(std::vector>& profiles) { if (AzureConnection::IsAzureConnectionAvailable()) { diff --git a/src/cascadia/TerminalSettingsModel/AzureCloudShellGenerator.h b/src/cascadia/TerminalSettingsModel/AzureCloudShellGenerator.h index 2f61624bc6f..28b79e93010 100644 --- a/src/cascadia/TerminalSettingsModel/AzureCloudShellGenerator.h +++ b/src/cascadia/TerminalSettingsModel/AzureCloudShellGenerator.h @@ -27,6 +27,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model std::wstring_view GetNamespace() const noexcept override; std::wstring_view GetDisplayName() const noexcept override; std::wstring_view GetIcon() const noexcept override { return {}; }; - void GenerateProfiles(std::vector>& profiles) const override; + void GenerateProfiles(std::vector>& profiles) override; }; }; diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp index 97b652a5aaa..8fc99ac9fa2 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp @@ -144,7 +144,7 @@ Model::CascadiaSettings CascadiaSettings::Copy() const Model::FragmentSettings FragmentSettings::Copy() const { - auto fragment{ winrt::make_self(_source, _json, _jsonSource) }; + auto fragment{ winrt::make_self(_source, _Json, _jsonSource) }; std::vector modifiedProfiles; modifiedProfiles.reserve(_modifiedProfiles.Size()); diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettings.h b/src/cascadia/TerminalSettingsModel/CascadiaSettings.h index 88a3133f96f..07cdc42dde4 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettings.h +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettings.h @@ -123,7 +123,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation void _appendProfile(winrt::com_ptr&& profile, const winrt::guid& guid, ParsedSettings& settings); void _addUserProfileParent(const winrt::com_ptr& profile); bool _addOrMergeUserColorScheme(const winrt::com_ptr& colorScheme); - void _executeGenerator(const IDynamicProfileGenerator& generator); + void _executeGenerator(IDynamicProfileGenerator& generator); + void _cleanupPowerShellInstaller(bool isPowerShellInstalled); winrt::com_ptr _registerFragment(const winrt::Microsoft::Terminal::Settings::Model::FragmentSettings& fragment, FragmentScope scope); std::unordered_set _ignoredNamespaces; @@ -237,14 +238,13 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation public: FragmentProfileEntry(winrt::guid profileGuid, hstring json) : _profileGuid{ profileGuid }, - _json{ json } {} + _Json{ json } {} winrt::guid ProfileGuid() const noexcept { return _profileGuid; } - hstring Json() const noexcept { return _json; } + WINRT_PROPERTY(hstring, Json); private: winrt::guid _profileGuid; - hstring _json; }; struct FragmentColorSchemeEntry : FragmentColorSchemeEntryT @@ -268,7 +268,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation public: FragmentSettings(hstring source, hstring json, hstring jsonSource) : _source{ source }, - _json{ json }, + _Json{ json }, _jsonSource{ jsonSource }, _modifiedProfiles{ winrt::single_threaded_vector() }, _newProfiles{ winrt::single_threaded_vector() }, @@ -277,12 +277,13 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation Model::FragmentSettings Copy() const; hstring Source() const noexcept { return _source; } - hstring Json() const noexcept { return _json; } hstring JsonSource() const noexcept { return _jsonSource; } Windows::Foundation::Collections::IVector ModifiedProfiles() const noexcept { return _modifiedProfiles; } Windows::Foundation::Collections::IVector NewProfiles() const noexcept { return _newProfiles; } Windows::Foundation::Collections::IVector ColorSchemes() const noexcept { return _colorSchemes; } + WINRT_PROPERTY(hstring, Json); + public: // views Windows::Foundation::Collections::IVectorView ModifiedProfilesView() const noexcept { return _modifiedProfiles.GetView(); } Windows::Foundation::Collections::IVectorView NewProfilesView() const noexcept { return _newProfiles.GetView(); } @@ -290,7 +291,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation private: hstring _source; - hstring _json; hstring _jsonSource; Windows::Foundation::Collections::IVector _modifiedProfiles; Windows::Foundation::Collections::IVector _newProfiles; diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp index 2a2d9af5c09..e47779cce91 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp @@ -19,6 +19,7 @@ #if TIL_FEATURE_DYNAMICSSHPROFILES_ENABLED #include "SshHostGenerator.h" #endif +#include "PowershellInstallationProfileGenerator.h" #include "ApplicationState.h" #include "DefaultTerminal.h" @@ -178,13 +179,77 @@ SettingsLoader::SettingsLoader(const std::string_view& userJSON, const std::stri // (meaning profiles specified by the application rather by the user). void SettingsLoader::GenerateProfiles() { - _executeGenerator(PowershellCoreProfileGenerator{}); - _executeGenerator(WslDistroGenerator{}); - _executeGenerator(AzureCloudShellGenerator{}); - _executeGenerator(VisualStudioGenerator{}); + PowershellCoreProfileGenerator powerShellGenerator{}; + _executeGenerator(powerShellGenerator); + + WslDistroGenerator wslGenerator{}; + _executeGenerator(wslGenerator); + + AzureCloudShellGenerator acsGenerator{}; + _executeGenerator(acsGenerator); + + VisualStudioGenerator vsGenerator{}; + _executeGenerator(vsGenerator); + #if TIL_FEATURE_DYNAMICSSHPROFILES_ENABLED - _executeGenerator(SshHostGenerator{}); + SshHostGenerator sshGenerator{}; + _executeGenerator(sshGenerator); #endif + + PowershellInstallationProfileGenerator pwshInstallationGenerator{}; + _executeGenerator(pwshInstallationGenerator); + + _cleanupPowerShellInstaller(!powerShellGenerator.GetPowerShellInstances().empty()); +} + +// Retrieve the "Install Latest PowerShell" profile and... +// - add a comment to the JSON to indicate it's conditionally applied +// - (if PowerShell is installed) mark it for deletion +void SettingsLoader::_cleanupPowerShellInstaller(bool isPowerShellInstalled) +{ + const hstring pwshInstallerNamespace{ PowershellInstallationProfileGenerator::Namespace }; + if (extensionPackageMap.contains(pwshInstallerNamespace)) + { + if (const auto& fragExtList = extensionPackageMap[pwshInstallerNamespace]->Fragments(); fragExtList.Size() > 0) + { + Json::StreamWriterBuilder styledWriter; + styledWriter["indentation"] = " "; + styledWriter["commentStyle"] = "All"; + + auto fragExt = get_self(fragExtList.GetAt(0)); + + // We want the comment to be the first thing in the object, + // "closeOnExit" is the first property, so target that. + auto fragExtJson = _parseJSON(til::u16u8(fragExt->Json())); + fragExtJson[JsonKey(ProfilesKey)][0]["closeOnExit"].setComment(til::u16u8(fmt::format(FMT_COMPILE(L"// {}"), RS_(L"PowerShellInstallationProfileJsonComment"))), Json::CommentPlacement::commentBefore); + fragExt->Json(hstring{ til::u8u16(Json::writeString(styledWriter, fragExtJson)) }); + + if (const auto& profileEntryList = fragExt->NewProfilesView(); profileEntryList.Size() > 0) + { + auto profileEntry = get_self(profileEntryList.GetAt(0)); + + // We want the comment to be the first thing in the object, + // "closeOnExit" is the first property, so target that. + auto profileJson = _parseJSON(til::u16u8(profileEntry->Json())); + profileJson["closeOnExit"].setComment(til::u16u8(fmt::format(FMT_COMPILE(L"// {}"), RS_(L"PowerShellInstallationProfileJsonComment"))), Json::CommentPlacement::commentBefore); + profileEntry->Json(hstring{ til::u8u16(Json::writeString(styledWriter, profileJson)) }); + + // If PowerShell is installed, mark the installer profile for deletion + if (isPowerShellInstalled) + { + const auto profileGuid = profileEntryList.GetAt(0).ProfileGuid(); + for (const auto& profile : userSettings.profiles) + { + if (profile->Guid() == profileGuid) + { + profile->Deleted(true); + break; + } + } + } + } + } + } } // A new settings.json gets a special treatment: @@ -993,7 +1058,7 @@ bool SettingsLoader::_addOrMergeUserColorScheme(const winrt::com_ptr> generatedProfiles; @@ -1590,7 +1655,11 @@ void CascadiaSettings::_resolveNewTabMenuProfiles() const auto activeProfileCount = gsl::narrow_cast(_activeProfiles.Size()); for (auto profileIndex = 0; profileIndex < activeProfileCount; profileIndex++) { - remainingProfilesMap.emplace(profileIndex, _activeProfiles.GetAt(profileIndex)); + const auto& profile = _activeProfiles.GetAt(profileIndex); + if (!profile.Deleted()) + { + remainingProfilesMap.emplace(profileIndex, _activeProfiles.GetAt(profileIndex)); + } } // We keep track of the "remaining profiles" - those that have not yet been resolved diff --git a/src/cascadia/TerminalSettingsModel/IDynamicProfileGenerator.h b/src/cascadia/TerminalSettingsModel/IDynamicProfileGenerator.h index 9144bcb3259..5ea68f2e3bd 100644 --- a/src/cascadia/TerminalSettingsModel/IDynamicProfileGenerator.h +++ b/src/cascadia/TerminalSettingsModel/IDynamicProfileGenerator.h @@ -32,6 +32,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model virtual std::wstring_view GetNamespace() const noexcept = 0; virtual std::wstring_view GetDisplayName() const noexcept = 0; virtual std::wstring_view GetIcon() const noexcept = 0; - virtual void GenerateProfiles(std::vector>& profiles) const = 0; + virtual void GenerateProfiles(std::vector>& profiles) = 0; }; }; diff --git a/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj b/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj index adf1610f5e1..9b8937a4a5a 100644 --- a/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj +++ b/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj @@ -90,6 +90,7 @@ KeyChordSerialization.idl + Profile.idl @@ -167,6 +168,7 @@ KeyChordSerialization.idl + Profile.idl diff --git a/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj.filters b/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj.filters index 3933a242819..be3dd79437f 100644 --- a/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj.filters +++ b/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj.filters @@ -15,6 +15,9 @@ profileGeneration + + profileGeneration + profileGeneration @@ -57,6 +60,9 @@ profileGeneration + + profileGeneration + profileGeneration diff --git a/src/cascadia/TerminalSettingsModel/PowershellCoreProfileGenerator.cpp b/src/cascadia/TerminalSettingsModel/PowershellCoreProfileGenerator.cpp index 196dc7b36a0..8ea4ab35f2a 100644 --- a/src/cascadia/TerminalSettingsModel/PowershellCoreProfileGenerator.cpp +++ b/src/cascadia/TerminalSettingsModel/PowershellCoreProfileGenerator.cpp @@ -24,336 +24,301 @@ static constexpr std::wstring_view POWERSHELL_ICON{ L"ms-appx:///ProfileIcons/pw static constexpr std::wstring_view POWERSHELL_PREVIEW_ICON{ L"ms-appx:///ProfileIcons/pwsh-preview.png" }; static constexpr std::wstring_view POWERSHELL_PREFERRED_PROFILE_NAME{ L"PowerShell" }; -namespace +using namespace ::Microsoft::Terminal::Settings::Model; + +namespace winrt::Microsoft::Terminal::Settings::Model { - enum PowerShellFlags + DEFINE_ENUM_FLAG_OPERATORS(PowershellCoreProfileGenerator::PowerShellFlags); + + constexpr bool PowershellCoreProfileGenerator::PowerShellInstance::operator<(const PowerShellInstance& second) const { - 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 + 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 PowershellCoreProfileGenerator::PowerShellInstance::Name() const { - int majorVersion; // 0 = we don't know, sort last. - PowerShellFlags flags; - std::filesystem::path executablePath; + std::wstringstream namestream; + namestream << L"PowerShell"; - constexpr bool operator<(const PowerShellInstance& second) const + if (WI_IsFlagSet(flags, PowerShellFlags::Store)) { - if (majorVersion != second.majorVersion) + if (WI_IsFlagSet(flags, PowerShellFlags::Preview)) { - return majorVersion < second.majorVersion; + namestream << L" Preview"; } - 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 + namestream << L" (msix)"; } - - // Method Description: - // - Generates a name, based on flags, for a powershell instance. - // Return value: - // - the name - std::wstring Name() const + else if (WI_IsFlagSet(flags, PowerShellFlags::Dotnet)) { - std::wstringstream namestream; - namestream << L"PowerShell"; - - if (WI_IsFlagSet(flags, PowerShellFlags::Store)) + namestream << L" (dotnet global)"; + } + else if (WI_IsFlagSet(flags, PowerShellFlags::Scoop)) + { + namestream << L" (scoop)"; + } + else + { + if (majorVersion < 7) { - if (WI_IsFlagSet(flags, PowerShellFlags::Preview)) - { - namestream << L" Preview"; - } - namestream << L" (msix)"; + namestream << L" Core"; } - else if (WI_IsFlagSet(flags, PowerShellFlags::Dotnet)) + if (majorVersion != 0) { - namestream << L" (dotnet global)"; + namestream << L" " << majorVersion; } - else if (WI_IsFlagSet(flags, PowerShellFlags::Scoop)) + if (WI_IsFlagSet(flags, PowerShellFlags::Preview)) { - namestream << L" (scoop)"; + namestream << L" Preview"; } - else + if (WI_IsFlagSet(flags, PowerShellFlags::WOWx86)) { - 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)"; - } + namestream << L" (x86)"; + } + if (WI_IsFlagSet(flags, PowerShellFlags::WOWARM)) + { + namestream << L" (ARM)"; } - return namestream.str(); } - }; -} + 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& out) -{ - const std::filesystem::path root{ wil::ExpandEnvironmentStringsW(directory.data()) }; - if (std::filesystem::exists(root)) + // 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, PowershellCoreProfileGenerator::PowerShellFlags flags, std::vector& out) { - for (const auto& versionedDir : std::filesystem::directory_iterator(root)) + const std::filesystem::path root{ wil::ExpandEnvironmentStringsW(directory.data()) }; + if (std::filesystem::exists(root)) { - const auto versionedPath = versionedDir.path(); - const auto executable = versionedPath / PWSH_EXE; - if (std::filesystem::exists(executable)) + for (const auto& versionedDir : std::filesystem::directory_iterator(root)) { - 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 }); + 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 ? PowershellCoreProfileGenerator::PowerShellFlags::Preview : PowershellCoreProfileGenerator::PowerShellFlags::None; + out.emplace_back(PowershellCoreProfileGenerator::PowerShellInstance{ std::stoi(versionedPath.filename()), + PowershellCoreProfileGenerator::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()) + // 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; } - 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& out) -{ - wil::unique_cotaskmem_string localAppDataFolder; - if (FAILED(SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &localAppDataFolder))) + // 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& out) { - return; - } + 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"; + 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)) + if (std::filesystem::exists(appExecAliasPath)) { - const auto previewPackage = _getStorePackage(POWERSHELL_PREVIEW_PFN); - if (previewPackage) + // App execution aliases for preview powershell + const auto previewPath = appExecAliasPath / POWERSHELL_PREVIEW_PFN; + if (std::filesystem::exists(previewPath)) { - out.emplace_back(PowerShellInstance{ - gsl::narrow_cast(previewPackage.Id().Version().Major), - PowerShellFlags::Store | PowerShellFlags::Preview, - previewPath / PWSH_EXE }); + const auto previewPackage = _getStorePackage(POWERSHELL_PREVIEW_PFN); + if (previewPackage) + { + out.emplace_back(PowershellCoreProfileGenerator::PowerShellInstance{ + gsl::narrow_cast(previewPackage.Id().Version().Major), + PowershellCoreProfileGenerator::PowerShellFlags::Store | PowershellCoreProfileGenerator::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) + // App execution aliases for stable powershell + const auto gaPath = appExecAliasPath / POWERSHELL_PFN; + if (std::filesystem::exists(gaPath)) { - out.emplace_back(PowerShellInstance{ - gaPackage.Id().Version().Major, - PowerShellFlags::Store, - gaPath / PWSH_EXE, - }); + const auto gaPackage = _getStorePackage(POWERSHELL_PFN); + if (gaPackage) + { + out.emplace_back(PowershellCoreProfileGenerator::PowerShellInstance{ + gaPackage.Id().Version().Major, + PowershellCoreProfileGenerator::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& out) -{ - const std::filesystem::path root{ wil::ExpandEnvironmentStringsW(directory.data()) }; - const auto pwshPath = root / PWSH_EXE; - if (std::filesystem::exists(pwshPath)) + // 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 PowershellCoreProfileGenerator::PowerShellFlags flags, std::vector& out) { - out.emplace_back(PowerShellInstance{ 0 /* we can't tell */, flags, pwshPath }); + const std::filesystem::path root{ wil::ExpandEnvironmentStringsW(directory.data()) }; + const auto pwshPath = root / PWSH_EXE; + if (std::filesystem::exists(pwshPath)) + { + out.emplace_back(PowershellCoreProfileGenerator::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 _collectPowerShellInstances() -{ - std::vector versions; + // Function Description: + // - Builds a comprehensive priority-ordered list of powershell instances. + // Return value: + // - a comprehensive priority-ordered list of powershell instances. + static std::vector _collectPowerShellInstances() + { + std::vector versions; - _accumulateTraditionalLayoutPowerShellInstancesInDirectory(L"%ProgramFiles%\\PowerShell", PowerShellFlags::None, versions); + _accumulateTraditionalLayoutPowerShellInstancesInDirectory(L"%ProgramFiles%\\PowerShell", PowershellCoreProfileGenerator::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); + _accumulateTraditionalLayoutPowerShellInstancesInDirectory(L"%ProgramFiles(x86)%\\PowerShell", PowershellCoreProfileGenerator::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); + _accumulateTraditionalLayoutPowerShellInstancesInDirectory(L"%ProgramFiles(Arm)%\\PowerShell", PowerShellFlags::WOWARM, versions); #endif - _accumulateStorePowerShellInstances(versions); + _accumulateStorePowerShellInstances(versions); - _accumulatePwshExeInDirectory(L"%USERPROFILE%\\.dotnet\\tools", PowerShellFlags::Dotnet, versions); - _accumulatePwshExeInDirectory(L"%USERPROFILE%\\scoop\\shims", PowerShellFlags::Scoop, versions); + _accumulatePwshExeInDirectory(L"%USERPROFILE%\\.dotnet\\tools", PowershellCoreProfileGenerator::PowerShellFlags::Dotnet, versions); + _accumulatePwshExeInDirectory(L"%USERPROFILE%\\scoop\\shims", PowershellCoreProfileGenerator::PowerShellFlags::Scoop, versions); - std::sort(versions.rbegin(), versions.rend()); // sort in reverse (best first) + 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 } }; + return versions; + } -std::wstring_view PowershellCoreProfileGenerator::GetNamespace() const noexcept -{ - return PowershellCoreGeneratorNamespace; -} + // 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::GetDisplayName() const noexcept -{ - return RS_(L"PowershellCoreProfileGeneratorDisplayName"); -} + std::wstring_view PowershellCoreProfileGenerator::GetNamespace() const noexcept + { + return PowershellCoreGeneratorNamespace; + } -std::wstring_view PowershellCoreProfileGenerator::GetIcon() const noexcept -{ - return POWERSHELL_ICON; -} + std::wstring_view PowershellCoreProfileGenerator::GetDisplayName() const noexcept + { + return RS_(L"PowershellCoreProfileGeneratorDisplayName"); + } -// Method Description: -// - Checks if pwsh is installed, and if it is, creates a profile to launch it. -// Arguments: -// - -// Return Value: -// - a vector with the PowerShell Core profile, if available. -void PowershellCoreProfileGenerator::GenerateProfiles(std::vector>& profiles) const -{ - const auto psInstances = _collectPowerShellInstances(); - auto first = true; + std::wstring_view PowershellCoreProfileGenerator::GetIcon() const noexcept + { + return POWERSHELL_ICON; + } - for (const auto& psI : psInstances) + // Method Description: + // - Checks if pwsh is installed, and if it is, creates a profile to launch it. + // Arguments: + // - + // Return Value: + // - a vector with the PowerShell Core profile, if available. + void PowershellCoreProfileGenerator::GenerateProfiles(std::vector>& profiles) { - 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) + GetPowerShellInstances(); + auto first = true; + + for (const auto& psI : _powerShellInstances) { - // 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 }); + 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; + } - first = false; + profiles.emplace_back(std::move(profile)); } + } - profiles.emplace_back(std::move(profile)); + std::vector PowershellCoreProfileGenerator::GetPowerShellInstances() noexcept + { + if (_powerShellInstances.empty()) + { + _powerShellInstances = _collectPowerShellInstances(); + } + return _powerShellInstances; } -} -// 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; + // 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; + } } diff --git a/src/cascadia/TerminalSettingsModel/PowershellCoreProfileGenerator.h b/src/cascadia/TerminalSettingsModel/PowershellCoreProfileGenerator.h index d473292e151..77f2aaa7a02 100644 --- a/src/cascadia/TerminalSettingsModel/PowershellCoreProfileGenerator.h +++ b/src/cascadia/TerminalSettingsModel/PowershellCoreProfileGenerator.h @@ -25,9 +25,60 @@ namespace winrt::Microsoft::Terminal::Settings::Model public: static const std::wstring_view GetPreferredPowershellProfileName(); + 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 + }; + + struct PowerShellInstance + { + int majorVersion; // 0 = we don't know, sort last. + PowerShellFlags flags; + std::filesystem::path executablePath; + + constexpr bool operator<(const PowerShellInstance& second) const; + std::wstring Name() const; + }; + std::wstring_view GetNamespace() const noexcept override; std::wstring_view GetDisplayName() const noexcept override; std::wstring_view GetIcon() const noexcept override; - void GenerateProfiles(std::vector>& profiles) const override; + void GenerateProfiles(std::vector>& profiles) override; + std::vector GetPowerShellInstances() noexcept; + + private: + std::vector _powerShellInstances; }; }; diff --git a/src/cascadia/TerminalSettingsModel/PowershellInstallationProfileGenerator.cpp b/src/cascadia/TerminalSettingsModel/PowershellInstallationProfileGenerator.cpp new file mode 100644 index 00000000000..3ad7e795b19 --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/PowershellInstallationProfileGenerator.cpp @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" + +#include "PowershellInstallationProfileGenerator.h" +#include "DynamicProfileUtils.h" + +#include + +static constexpr std::wstring_view POWERSHELL_ICON{ L"ms-appx:///ProfileIcons/pwsh.png" }; +static constexpr std::wstring_view POWERSHELL_ICON_64{ L"ms-appx:///ProfileIcons/Powershell_black_64.png" }; + +namespace winrt::Microsoft::Terminal::Settings::Model +{ + std::wstring_view PowershellInstallationProfileGenerator::Namespace{ L"Windows.Terminal.InstallPowerShell" }; + + std::wstring_view PowershellInstallationProfileGenerator::GetNamespace() const noexcept + { + return Namespace; + } + + std::wstring_view PowershellInstallationProfileGenerator::GetDisplayName() const noexcept + { + return RS_(L"PowerShellInstallationProfileGeneratorDisplayName"); + } + + std::wstring_view PowershellInstallationProfileGenerator::GetIcon() const noexcept + { + return POWERSHELL_ICON_64; + } + + void PowershellInstallationProfileGenerator::GenerateProfiles(std::vector>& profiles) + { + auto profile{ CreateDynamicProfile(RS_(L"PowerShellInstallationProfileName")) }; + profile->Commandline(winrt::hstring{ fmt::format(FMT_COMPILE(L"cmd /k winget install --interactive --id Microsoft.PowerShell & echo. & echo {} & exit"), RS_(L"PowerShellInstallationInstallerGuidance")) }); + profile->Icon(winrt::hstring{ POWERSHELL_ICON }); + profile->CloseOnExit(CloseOnExitMode::Never); + + profiles.emplace_back(std::move(profile)); + } +} diff --git a/src/cascadia/TerminalSettingsModel/PowershellInstallationProfileGenerator.h b/src/cascadia/TerminalSettingsModel/PowershellInstallationProfileGenerator.h new file mode 100644 index 00000000000..6a3fc2fffff --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/PowershellInstallationProfileGenerator.h @@ -0,0 +1,33 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- PowershellInstallationProfileGenerator + +Abstract: +- This is the dynamic profile generator for a PowerShell stub. Checks if pwsh is + installed, and if it is NOT installed, creates a profile that installs the + latest PowerShell. + +Author(s): +- Carlos Zamora - March 2025 + +--*/ + +#pragma once + +#include "IDynamicProfileGenerator.h" + +namespace winrt::Microsoft::Terminal::Settings::Model +{ + class PowershellInstallationProfileGenerator final : public IDynamicProfileGenerator + { + public: + static std::wstring_view Namespace; + std::wstring_view GetNamespace() const noexcept override; + std::wstring_view GetDisplayName() const noexcept override; + std::wstring_view GetIcon() const noexcept override; + void GenerateProfiles(std::vector>& profiles) override; + }; +}; diff --git a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw index 37d9b2629b4..07758f4967d 100644 --- a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw @@ -748,6 +748,14 @@ PowerShell Profile Generator The display name of a dynamic profile generator for PowerShell + + PowerShell Installation Generator + The display name of a dynamic profile generator that installs the latest PowerShell + + + Install Latest PowerShell + The display name of a profile generated by the PowerShellInstallationProfileGenerator. This profile installs the latest PowerShell. + Azure Cloud Shell Profile Generator The display name of a dynamic profile generator for Azure Cloud Shell @@ -760,4 +768,11 @@ SSH Host Profile Generator The display name of a dynamic profile generator for SSH hosts - \ No newline at end of file + + Restart Windows Terminal to apply the new profile. + Guidance displayed by the installer directing the user to restart the app. + + + This profile only appears if PowerShell is not installed + + diff --git a/src/cascadia/TerminalSettingsModel/SshHostGenerator.cpp b/src/cascadia/TerminalSettingsModel/SshHostGenerator.cpp index 2bd52789959..9276ac63520 100644 --- a/src/cascadia/TerminalSettingsModel/SshHostGenerator.cpp +++ b/src/cascadia/TerminalSettingsModel/SshHostGenerator.cpp @@ -149,7 +149,7 @@ std::wstring_view SshHostGenerator::GetIcon() const noexcept // - // Return Value: // - -void SshHostGenerator::GenerateProfiles(std::vector>& profiles) const +void SshHostGenerator::GenerateProfiles(std::vector>& profiles) { std::wstring sshExePath; if (_tryFindSshExePath(sshExePath)) diff --git a/src/cascadia/TerminalSettingsModel/SshHostGenerator.h b/src/cascadia/TerminalSettingsModel/SshHostGenerator.h index d83e62535ba..2bce0471612 100644 --- a/src/cascadia/TerminalSettingsModel/SshHostGenerator.h +++ b/src/cascadia/TerminalSettingsModel/SshHostGenerator.h @@ -26,7 +26,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model std::wstring_view GetNamespace() const noexcept override; std::wstring_view GetDisplayName() const noexcept override; std::wstring_view GetIcon() const noexcept override; - void GenerateProfiles(std::vector>& profiles) const override; + void GenerateProfiles(std::vector>& profiles) override; private: static const std::wregex _configKeyValueRegex; diff --git a/src/cascadia/TerminalSettingsModel/VisualStudioGenerator.cpp b/src/cascadia/TerminalSettingsModel/VisualStudioGenerator.cpp index ad09059f9e9..39a40d65bbb 100644 --- a/src/cascadia/TerminalSettingsModel/VisualStudioGenerator.cpp +++ b/src/cascadia/TerminalSettingsModel/VisualStudioGenerator.cpp @@ -22,7 +22,7 @@ std::wstring_view VisualStudioGenerator::GetDisplayName() const noexcept return RS_(L"VisualStudioGeneratorDisplayName"); } -void VisualStudioGenerator::GenerateProfiles(std::vector>& profiles) const +void VisualStudioGenerator::GenerateProfiles(std::vector>& profiles) { const auto instances = VsSetupConfiguration::QueryInstances(); diff --git a/src/cascadia/TerminalSettingsModel/VisualStudioGenerator.h b/src/cascadia/TerminalSettingsModel/VisualStudioGenerator.h index 2f5c2274c6c..b256e076f85 100644 --- a/src/cascadia/TerminalSettingsModel/VisualStudioGenerator.h +++ b/src/cascadia/TerminalSettingsModel/VisualStudioGenerator.h @@ -30,7 +30,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model std::wstring_view GetNamespace() const noexcept override; std::wstring_view GetDisplayName() const noexcept override; std::wstring_view GetIcon() const noexcept override { return {}; }; - void GenerateProfiles(std::vector>& profiles) const override; + void GenerateProfiles(std::vector>& profiles) override; class IVisualStudioProfileGenerator { diff --git a/src/cascadia/TerminalSettingsModel/WslDistroGenerator.cpp b/src/cascadia/TerminalSettingsModel/WslDistroGenerator.cpp index f571b08f668..715582fab4d 100644 --- a/src/cascadia/TerminalSettingsModel/WslDistroGenerator.cpp +++ b/src/cascadia/TerminalSettingsModel/WslDistroGenerator.cpp @@ -238,7 +238,7 @@ static bool getWslNames(const wil::unique_hkey& wslRootKey, // - // Return Value: // - A list of WSL profiles. -void WslDistroGenerator::GenerateProfiles(std::vector>& profiles) const +void WslDistroGenerator::GenerateProfiles(std::vector>& profiles) { auto wslRootKey{ openWslRegKey() }; if (wslRootKey) diff --git a/src/cascadia/TerminalSettingsModel/WslDistroGenerator.h b/src/cascadia/TerminalSettingsModel/WslDistroGenerator.h index 123734523fa..77cda13f355 100644 --- a/src/cascadia/TerminalSettingsModel/WslDistroGenerator.h +++ b/src/cascadia/TerminalSettingsModel/WslDistroGenerator.h @@ -26,6 +26,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model std::wstring_view GetNamespace() const noexcept override; std::wstring_view GetDisplayName() const noexcept override; std::wstring_view GetIcon() const noexcept override; - void GenerateProfiles(std::vector>& profiles) const override; + void GenerateProfiles(std::vector>& profiles) override; }; };