Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
019bb76
Add Extensions page to Settings UI
carlos-zamora Nov 13, 2024
7f0c9e5
rename some resources; remove some TODOs
carlos-zamora Feb 11, 2025
f11515e
add nested page
carlos-zamora Feb 14, 2025
75d02c2
add nested page (part 2) + support for multiple json files
carlos-zamora Feb 19, 2025
e6c43c0
copy over DisabledProfileSources
carlos-zamora Feb 19, 2025
6b83fa7
wil::to_vector
carlos-zamora Feb 25, 2025
5afc9bc
add dynamic profile generators
carlos-zamora Feb 25, 2025
7cdbb7c
address feedback
carlos-zamora Feb 27, 2025
b0a7ef1
apply feedback from Dustin
carlos-zamora Apr 21, 2025
0af4eb0
Fix XAML crash for release builds (applied incompatible styling)
carlos-zamora Apr 21, 2025
2766f21
JsonSource->Filename; remove deep copy of fragments & generators
carlos-zamora Apr 22, 2025
7fcdeaa
Add display name and icon to SUI extensions page
carlos-zamora Feb 26, 2025
b472d9f
add DisplayName and Icon to profile generators; improve identifier sy…
carlos-zamora Feb 27, 2025
1072d69
a11y check: screen reader, keyboard navigation, FastPass ✅
carlos-zamora Feb 27, 2025
c371c48
use high resolution assets
carlos-zamora Apr 16, 2025
e9f83fc
Explicitly introduce and use profile generator icons; add VS logo
carlos-zamora Apr 16, 2025
f25e6fe
[fix rebase] remove deep clone of ext pkgs
carlos-zamora Apr 22, 2025
2e7e37a
Introduce PowerShell installer stub
carlos-zamora Feb 28, 2025
a7d6e27
apply feedback
carlos-zamora Apr 17, 2025
cc1d632
fix ARM64
carlos-zamora Apr 18, 2025
0fe444e
add feature flag
carlos-zamora Apr 18, 2025
36d28e2
fix rebase
carlos-zamora Apr 21, 2025
057128e
Lazy load extension objects for SUI
carlos-zamora Apr 24, 2025
debacee
Add Badge to highlight new Extensions Page
carlos-zamora Apr 17, 2025
fc0eb78
simplify AppState code; display 'NEW'
carlos-zamora Apr 22, 2025
34c6115
Merge branch 'main' into dev/cazamor/sui/extensions-page
carlos-zamora Apr 28, 2025
2445500
Merge branch 'dev/cazamor/sui/extensions-page' into dev/cazamor/sui/e…
carlos-zamora Apr 29, 2025
3353323
apply feedback from bug bash
carlos-zamora May 2, 2025
2d077ca
Linguistic sorting!
carlos-zamora May 5, 2025
831313c
fix propagating CascadiaSettings reference & Enabled notification
carlos-zamora May 5, 2025
5ae13b3
Add toggle switches to root of Extensions page
carlos-zamora May 6, 2025
27e1aee
Merge branch 'main' into dev/cazamor/sui/extensions-page
carlos-zamora May 6, 2025
8db1805
Merge branch 'dev/cazamor/sui/extensions-page' into dev/cazamor/sui/e…
carlos-zamora May 6, 2025
a282164
spell
carlos-zamora May 6, 2025
ed88c69
scope the generator; fix SUI scenario (post-installation)
carlos-zamora May 14, 2025
81ffd97
Merge branch 'main' into dev/cazamor/sui/extensions-page
carlos-zamora May 23, 2025
ca32d58
apply suggestions from Dustin
carlos-zamora May 23, 2025
d38cc9a
Merge branch 'dev/cazamor/sui/extensions-page' into dev/cazamor/sui/e…
carlos-zamora May 27, 2025
a5a7aea
fix comment
carlos-zamora May 28, 2025
1cd660f
Merge branch 'main' into dev/cazamor/sui/extensions-page
carlos-zamora May 28, 2025
ffcce7f
Merge branch 'dev/cazamor/sui/extensions-page' into dev/cazamor/sui/e…
carlos-zamora May 28, 2025
ac717dc
Merge branch 'main' into dev/cazamor/sui/ext-page/powershell-stub
carlos-zamora May 28, 2025
abee0c6
format
carlos-zamora May 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/cascadia/TerminalApp/TabManagement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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))
{
Expand Down
3 changes: 2 additions & 1 deletion src/cascadia/TerminalSettingsEditor/Extensions.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,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)}">
<FontIcon Glyph="&#xE8A7;"
Style="{StaticResource SettingContainerFontIconStyle}" />
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ std::wstring_view AzureCloudShellGenerator::GetIcon() const noexcept
// - <none>
// Return Value:
// - a vector with the Azure Cloud Shell connection profile, if available.
void AzureCloudShellGenerator::GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles) const
void AzureCloudShellGenerator::GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles)
{
if (AzureConnection::IsAzureConnectionAvailable())
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
void GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles) const override;
void GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles) override;
};
};
14 changes: 7 additions & 7 deletions src/cascadia/TerminalSettingsModel/CascadiaSettings.h
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
void _appendProfile(winrt::com_ptr<Profile>&& profile, const winrt::guid& guid, ParsedSettings& settings);
void _addUserProfileParent(const winrt::com_ptr<implementation::Profile>& profile);
bool _addOrMergeUserColorScheme(const winrt::com_ptr<implementation::ColorScheme>& colorScheme);
static void _executeGenerator(const IDynamicProfileGenerator& generator, std::vector<winrt::com_ptr<implementation::Profile>>& profilesList);
static void _executeGenerator(IDynamicProfileGenerator& generator, std::vector<winrt::com_ptr<implementation::Profile>>& profilesList);
void _patchInstallPowerShellProfile(bool isPowerShellInstalled);
winrt::com_ptr<implementation::ExtensionPackage> _registerFragment(const winrt::Microsoft::Terminal::Settings::Model::FragmentSettings& fragment, FragmentScope scope);
Json::StreamWriterBuilder _getJsonStyledWriter();

Expand Down Expand Up @@ -248,14 +249,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; }
til::property<hstring> Json;

private:
winrt::guid _profileGuid;
hstring _json;
};

struct FragmentColorSchemeEntry : FragmentColorSchemeEntryT<FragmentColorSchemeEntry>
Expand All @@ -278,27 +278,27 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
public:
FragmentSettings(hstring source, hstring json, hstring filename) :
_source{ source },
_json{ json },
_Json{ json },
_filename{ filename } {}

hstring Source() const noexcept { return _source; }
hstring Json() const noexcept { return _json; }
hstring Filename() const noexcept { return _filename; }
Windows::Foundation::Collections::IVector<Model::FragmentProfileEntry> ModifiedProfiles() const noexcept { return _modifiedProfiles; }
void ModifiedProfiles(const Windows::Foundation::Collections::IVector<Model::FragmentProfileEntry>& modifiedProfiles) noexcept { _modifiedProfiles = modifiedProfiles; }
Windows::Foundation::Collections::IVector<Model::FragmentProfileEntry> NewProfiles() const noexcept { return _newProfiles; }
void NewProfiles(const Windows::Foundation::Collections::IVector<Model::FragmentProfileEntry>& newProfiles) noexcept { _newProfiles = newProfiles; }
Windows::Foundation::Collections::IVector<Model::FragmentColorSchemeEntry> ColorSchemes() const noexcept { return _colorSchemes; }
void ColorSchemes(const Windows::Foundation::Collections::IVector<Model::FragmentColorSchemeEntry>& colorSchemes) noexcept { _colorSchemes = colorSchemes; }
WINRT_PROPERTY(hstring, Json);

public:
// views
Windows::Foundation::Collections::IVectorView<Model::FragmentProfileEntry> ModifiedProfilesView() const noexcept { return _modifiedProfiles ? _modifiedProfiles.GetView() : nullptr; }
Windows::Foundation::Collections::IVectorView<Model::FragmentProfileEntry> NewProfilesView() const noexcept { return _newProfiles ? _newProfiles.GetView() : nullptr; }
Windows::Foundation::Collections::IVectorView<Model::FragmentColorSchemeEntry> ColorSchemesView() const noexcept { return _colorSchemes ? _colorSchemes.GetView() : nullptr; }

private:
hstring _source;
hstring _json;
hstring _filename;
Windows::Foundation::Collections::IVector<Model::FragmentProfileEntry> _modifiedProfiles;
Windows::Foundation::Collections::IVector<Model::FragmentProfileEntry> _newProfiles;
Expand Down
113 changes: 99 additions & 14 deletions src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#if TIL_FEATURE_DYNAMICSSHPROFILES_ENABLED
#include "SshHostGenerator.h"
#endif
#include "PowershellInstallationProfileGenerator.h"

#include "ApplicationState.h"
#include "DefaultTerminal.h"
Expand Down Expand Up @@ -210,28 +211,58 @@ Json::StreamWriterBuilder SettingsLoader::_getJsonStyledWriter()
// (meaning profiles specified by the application rather by the user).
void SettingsLoader::GenerateProfiles()
{
auto generateProfiles = [&](const IDynamicProfileGenerator& generator) {
auto generateProfiles = [&]<typename T>() {
T generator{};
if (!_ignoredNamespaces.contains(generator.GetNamespace()))
{
_executeGenerator(generator, inboxSettings.profiles);
}
return generator;
};

bool isPowerShellInstalled;
{
auto powerShellGenerator = generateProfiles.template operator()<PowershellCoreProfileGenerator>();
isPowerShellInstalled = !powerShellGenerator.GetPowerShellInstances().empty();
}

if (Feature_PowerShellInstallerProfileGenerator::IsEnabled())
{
if (isPowerShellInstalled)
{
// PowerShell is installed, mark the installer profile for deletion (if found)
const winrt::guid profileGuid{ L"{965a10f2-b0f2-55dc-a3c2-2ddbf639bf89}" };
for (const auto& profile : userSettings.profiles)
{
if (profile->Guid() == profileGuid)
{
profile->Deleted(true);
break;
}
}
}
else
{
// PowerShell isn't installed --> generate the installer stub profile
generateProfiles.template operator()<PowershellInstallationProfileGenerator>();
}
}

// Generate profiles for each generator and add them to the inbox settings.
// Be sure to update the same list below.
generateProfiles(PowershellCoreProfileGenerator{});
generateProfiles(WslDistroGenerator{});
generateProfiles(AzureCloudShellGenerator{});
generateProfiles(VisualStudioGenerator{});
generateProfiles.template operator()<WslDistroGenerator>();
generateProfiles.template operator()<AzureCloudShellGenerator>();
generateProfiles.template operator()<VisualStudioGenerator>();
#if TIL_FEATURE_DYNAMICSSHPROFILES_ENABLED
generateProfiles(SshHostGenerator{});
generateProfiles.template operator()<SshHostGenerator>();
#endif
}

// Generate ExtensionPackage objects from the profile generators.
void SettingsLoader::GenerateExtensionPackagesFromProfileGenerators()
{
auto generateExtensionPackages = [&](const IDynamicProfileGenerator& generator) {
auto generateExtensionPackages = [&]<typename T>() {
T generator{};
std::vector<winrt::com_ptr<implementation::Profile>> profilesList;
_executeGenerator(generator, profilesList);

Expand All @@ -256,19 +287,69 @@ void SettingsLoader::GenerateExtensionPackagesFromProfileGenerators()
auto extPkg = _registerFragment(std::move(*generatorExtension), FragmentScope::Machine);
extPkg->DisplayName(hstring{ generator.GetDisplayName() });
extPkg->Icon(hstring{ generator.GetIcon() });
return generator;
};

bool isPowerShellInstalled;
{
auto powerShellGenerator = generateExtensionPackages.template operator()<PowershellCoreProfileGenerator>();
isPowerShellInstalled = !powerShellGenerator.GetPowerShellInstances().empty();
}
if (Feature_PowerShellInstallerProfileGenerator::IsEnabled())
{
generateExtensionPackages.template operator()<PowershellInstallationProfileGenerator>();
_patchInstallPowerShellProfile(isPowerShellInstalled);
}

// Generate extension package objects for each generator.
// Be sure to update the same list above.
generateExtensionPackages(PowershellCoreProfileGenerator{});
generateExtensionPackages(WslDistroGenerator{});
generateExtensionPackages(AzureCloudShellGenerator{});
generateExtensionPackages(VisualStudioGenerator{});
generateExtensionPackages.template operator()<WslDistroGenerator>();
generateExtensionPackages.template operator()<AzureCloudShellGenerator>();
generateExtensionPackages.template operator()<VisualStudioGenerator>();
#if TIL_FEATURE_DYNAMICSSHPROFILES_ENABLED
generateExtensionPackages(SshHostGenerator{});
generateExtensionPackages.template operator()<SshHostGenerator>();
#endif
}

// Retrieve the "Install Latest PowerShell" profile and add a comment to the JSON to indicate it's conditionally applied.
// If PowerShell is installed, delete the profile from the extension package.
void SettingsLoader::_patchInstallPowerShellProfile(bool isPowerShellInstalled)
{
const hstring pwshInstallerNamespace{ PowershellInstallationProfileGenerator::Namespace };
if (extensionPackageMap.contains(pwshInstallerNamespace))
{
if (const auto& fragExtList = extensionPackageMap[pwshInstallerNamespace]->Fragments(); fragExtList.Size() > 0)
{
auto fragExt = get_self<FragmentSettings>(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()));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest, I kind of forgot how the extension code works, but this still seems kind of hacky to me.

The fragment in this case is based on an extension, right? Don't extensions generate profiles only? I'm missing the big picture how we go from profiles to JSON here, hence my confusion. Basically, since extensions generate profiles (including PowershellInstallationProfileGenerator), my assumption would be that this function doesn't need to deal with parsing JSON at all.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh this section is SUPER hacky! The sole purpose of this is to add the JSON comment so it appears like this:
image
image

So yeah, we have to parse the JSON, add the comment, then store the new JSON with the comment back in.
We have to do it twice:

  1. the settings.json blob representing the entire fragment extension
  2. the generated profile itself

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Figured it'd be good to try and tell the user that this profile only appears if PowerShell is not installed. It's a weird little behavior that I figured at least a JSON comment would help point out, even if not many people read the JSON comment.

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(_getJsonStyledWriter(), fragExtJson)) });

if (const auto& profileEntryList = fragExt->NewProfiles(); profileEntryList.Size() > 0)
{
if (isPowerShellInstalled)
{
// PowerShell is installed, so the installer profile was marked for deletion in GenerateProfiles().
// Remove the profile object from the fragment so it doesn't show up in the settings UI.
profileEntryList.RemoveAt(0);
}
else
{
// We want the comment to be the first thing in the object,
// "closeOnExit" is the first property, so target that.
auto profileEntry = get_self<FragmentProfileEntry>(profileEntryList.GetAt(0));
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(_getJsonStyledWriter(), profileJson)) });
}
}
}
}
}

// A new settings.json gets a special treatment:
// 1. The default profile is a PowerShell 7+ one, if one was generated,
// and falls back to the standard PowerShell 5 profile otherwise.
Expand Down Expand Up @@ -1095,7 +1176,7 @@ bool SettingsLoader::_addOrMergeUserColorScheme(const winrt::com_ptr<implementat

// As the name implies it executes a generator.
// Generated profiles are added to .inboxSettings. Used by GenerateProfiles().
void SettingsLoader::_executeGenerator(const IDynamicProfileGenerator& generator, std::vector<winrt::com_ptr<implementation::Profile>>& profilesList)
void SettingsLoader::_executeGenerator(IDynamicProfileGenerator& generator, std::vector<winrt::com_ptr<implementation::Profile>>& profilesList)
{
const auto generatorNamespace = generator.GetNamespace();
const auto previousSize = profilesList.size();
Expand Down Expand Up @@ -1679,7 +1760,11 @@ void CascadiaSettings::_resolveNewTabMenuProfiles() const
auto activeProfileCount = gsl::narrow_cast<int>(_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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<winrt::com_ptr<implementation::Profile>>& profiles) const = 0;
virtual void GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles) = 0;
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
<DependentUpon>KeyChordSerialization.idl</DependentUpon>
</ClInclude>
<ClInclude Include="PowershellCoreProfileGenerator.h" />
<ClInclude Include="PowershellInstallationProfileGenerator.h" />
<ClInclude Include="Profile.h">
<DependentUpon>Profile.idl</DependentUpon>
</ClInclude>
Expand Down Expand Up @@ -167,6 +168,7 @@
<DependentUpon>KeyChordSerialization.idl</DependentUpon>
</ClCompile>
<ClCompile Include="PowershellCoreProfileGenerator.cpp" />
<ClCompile Include="PowershellInstallationProfileGenerator.cpp" />
<ClCompile Include="Profile.cpp">
<DependentUpon>Profile.idl</DependentUpon>
</ClCompile>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
<ClCompile Include="PowershellCoreProfileGenerator.cpp">
<Filter>profileGeneration</Filter>
</ClCompile>
<ClCompile Include="PowershellInstallationProfileGenerator.cpp">
<Filter>profileGeneration</Filter>
</ClCompile>
<ClCompile Include="WslDistroGenerator.cpp">
<Filter>profileGeneration</Filter>
</ClCompile>
Expand Down Expand Up @@ -57,6 +60,9 @@
<ClInclude Include="PowershellCoreProfileGenerator.h">
<Filter>profileGeneration</Filter>
</ClInclude>
<ClInclude Include="PowershellInstallationProfileGenerator.h">
<Filter>profileGeneration</Filter>
</ClInclude>
<ClInclude Include="WslDistroGenerator.h">
<Filter>profileGeneration</Filter>
</ClInclude>
Expand Down
Loading
Loading