Skip to content
Open
Show file tree
Hide file tree
Changes from 22 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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/cascadia/CascadiaResources.build.items
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@
<DeploymentContent>true</DeploymentContent>
<Link>ProfileIcons\%(RecursiveDir)%(FileName)%(Extension)</Link>
</Content>
<!-- Profile Generator Icons -->
<Content Include="$(OpenConsoleDir)src\cascadia\CascadiaPackage\ProfileGeneratorIcons\**\*">
<DeploymentContent>true</DeploymentContent>
<Link>ProfileGeneratorIcons\%(RecursiveDir)%(FileName)%(Extension)</Link>
</Content>
<!-- Default Settings -->
<Content Include="$(OpenConsoleDir)src\cascadia\TerminalSettingsModel\defaults.json">
<DeploymentContent>true</DeploymentContent>
Expand Down
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
2 changes: 1 addition & 1 deletion src/cascadia/TerminalSettingsEditor/AddProfile.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
<IconSourceElement Grid.Column="0"
Width="16"
Height="16"
IconSource="{x:Bind mtu:IconPathConverter.IconSourceWUX(Icon), Mode=OneTime}" />
IconSource="{x:Bind mtu:IconPathConverter.IconSourceWUX(EvaluatedIcon), Mode=OneTime}" />

<TextBlock Grid.Column="1"
Text="{x:Bind Name}" />
Expand Down
5 changes: 3 additions & 2 deletions src/cascadia/TerminalSettingsEditor/CommonResources.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -1199,7 +1199,7 @@
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}" />
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Template">
<Setter.Value>
Expand All @@ -1222,7 +1222,8 @@
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
ContentTransitions="{TemplateBinding ContentTransitions}" />
<FontIcon Margin="20,0,8,0"
<FontIcon Grid.Column="1"
Margin="20,0,8,0"
HorizontalAlignment="Right"
FontSize="10"
FontWeight="Black"
Expand Down
336 changes: 336 additions & 0 deletions src/cascadia/TerminalSettingsEditor/Extensions.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,336 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

#include "pch.h"
#include "Extensions.h"
#include "Extensions.g.cpp"
#include "ExtensionPackageViewModel.g.cpp"
#include "ExtensionsViewModel.g.cpp"
#include "FragmentProfileViewModel.g.cpp"
#include "ExtensionPackageTemplateSelector.g.cpp"

#include <LibraryResources.h>
#include "..\WinRTUtils\inc\Utils.h"

using namespace winrt::Windows::Foundation;
using namespace winrt::Windows::Foundation::Collections;
using namespace winrt::Windows::UI::Xaml;
using namespace winrt::Windows::UI::Xaml::Controls;
using namespace winrt::Windows::UI::Xaml::Navigation;

namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
Extensions::Extensions()
{
InitializeComponent();

_extensionPackageIdentifierTemplateSelector = Resources().Lookup(box_value(L"ExtensionPackageIdentifierTemplateSelector")).as<Editor::ExtensionPackageTemplateSelector>();

Automation::AutomationProperties::SetName(ActiveExtensionsList(), RS_(L"Extensions_ActiveExtensionsHeader/Text"));
Automation::AutomationProperties::SetName(ModifiedProfilesList(), RS_(L"Extensions_ModifiedProfilesHeader/Text"));
Automation::AutomationProperties::SetName(AddedProfilesList(), RS_(L"Extensions_AddedProfilesHeader/Text"));
Automation::AutomationProperties::SetName(AddedColorSchemesList(), RS_(L"Extensions_AddedColorSchemesHeader/Text"));
}

void Extensions::OnNavigatedTo(const NavigationEventArgs& e)
{
_ViewModel = e.Parameter().as<Editor::ExtensionsViewModel>();
get_self<ExtensionsViewModel>(_ViewModel)->ExtensionPackageIdentifierTemplateSelector(_extensionPackageIdentifierTemplateSelector);
}

void Extensions::ExtensionNavigator_Click(const IInspectable& sender, const RoutedEventArgs& /*args*/)
{
const auto extPkgVM = sender.as<Controls::Button>().Tag().as<Editor::ExtensionPackageViewModel>();
_ViewModel.CurrentExtensionPackage(extPkgVM);
}

void Extensions::NavigateToProfile_Click(const IInspectable& sender, const RoutedEventArgs& /*args*/)
{
const auto& profileGuid = sender.as<Controls::Button>().Tag().as<guid>();
get_self<ExtensionsViewModel>(_ViewModel)->NavigateToProfile(profileGuid);
}

void Extensions::NavigateToColorScheme_Click(const IInspectable& sender, const RoutedEventArgs& /*args*/)
{
const auto& schemeVM = sender.as<Controls::Button>().Tag().as<Editor::ColorSchemeViewModel>();
get_self<ExtensionsViewModel>(_ViewModel)->NavigateToColorScheme(schemeVM);
}

ExtensionsViewModel::ExtensionsViewModel(const Model::CascadiaSettings& settings, const Editor::ColorSchemesPageViewModel& colorSchemesPageVM) :
_settings{ settings },
_colorSchemesPageVM{ colorSchemesPageVM }
{
UpdateSettings(settings, colorSchemesPageVM);

PropertyChanged([this](auto&&, const PropertyChangedEventArgs& args) {
const auto viewModelProperty{ args.PropertyName() };
if (viewModelProperty == L"CurrentExtensionPackage")
{
// Update the views to reflect the current extension package, if one is selected.
// Otherwise, show components from all extensions
_profilesModifiedView.Clear();
_profilesAddedView.Clear();
_colorSchemesAddedView.Clear();

// Helper lambda to add the contents of an extension package to the current view
auto addPackageContentsToView = [&](const Editor::ExtensionPackageViewModel& extPkg) {
auto extPkgVM = get_self<ExtensionPackageViewModel>(extPkg);
for (const auto& ext : extPkgVM->FragmentExtensions())
{
for (const auto& profile : ext.ProfilesModified())
{
_profilesModifiedView.Append(profile);
}
for (const auto& profile : ext.ProfilesAdded())
{
_profilesAddedView.Append(profile);
}
for (const auto& scheme : ext.ColorSchemesAdded())
{
_colorSchemesAddedView.Append(scheme);
}
}
};

if (const auto currentExtensionPackage = CurrentExtensionPackage())
{
addPackageContentsToView(currentExtensionPackage);
}
else
{
for (const auto& extPkg : _extensionPackages)
{
addPackageContentsToView(extPkg);
}
}

_NotifyChanges(L"IsExtensionView", L"CurrentExtensionPackageIdentifierTemplate");
}
});
}

void ExtensionsViewModel::UpdateSettings(const Model::CascadiaSettings& settings, const Editor::ColorSchemesPageViewModel& colorSchemesPageVM)
{
_settings = settings;
_colorSchemesPageVM = colorSchemesPageVM;
_CurrentExtensionPackage = nullptr;

std::vector<Model::ExtensionPackage> extensions = wil::to_vector(settings.Extensions());

// these vectors track components all extensions successfully added
std::vector<Editor::ExtensionPackageViewModel> extensionPackages;
std::vector<Editor::FragmentProfileViewModel> profilesModifiedTotal;
std::vector<Editor::FragmentProfileViewModel> profilesAddedTotal;
std::vector<Editor::FragmentColorSchemeViewModel> colorSchemesAddedTotal;
for (const auto& extPkg : extensions)
{
auto extPkgVM = winrt::make_self<ExtensionPackageViewModel>(extPkg, settings);
extensionPackages.push_back(*extPkgVM);
for (const auto& fragExt : extPkg.FragmentsView())
{
const auto extensionEnabled = GetExtensionState(fragExt.Source(), _settings);

// these vectors track everything the current extension attempted to bring in
std::vector<Editor::FragmentProfileViewModel> currentProfilesModified;
std::vector<Editor::FragmentProfileViewModel> currentProfilesAdded;
std::vector<Editor::FragmentColorSchemeViewModel> currentColorSchemesAdded;

if (fragExt.ModifiedProfilesView())
{
for (const auto&& entry : fragExt.ModifiedProfilesView())
{
// Ensure entry successfully modifies a profile before creating and registering the object
if (const auto& deducedProfile = _settings.FindProfile(entry.ProfileGuid()))
{
auto vm = winrt::make<FragmentProfileViewModel>(entry, fragExt, deducedProfile);
currentProfilesModified.push_back(vm);
if (extensionEnabled)
{
profilesModifiedTotal.push_back(vm);
}
}
}
}

if (fragExt.NewProfilesView())
{
for (const auto&& entry : fragExt.NewProfilesView())
{
// Ensure entry successfully points to a profile before creating and registering the object.
// The profile may have been removed by the user.
if (const auto& deducedProfile = _settings.FindProfile(entry.ProfileGuid()))
{
auto vm = winrt::make<FragmentProfileViewModel>(entry, fragExt, deducedProfile);
currentProfilesAdded.push_back(vm);
if (extensionEnabled)
{
profilesAddedTotal.push_back(vm);
}
}
}
}

if (fragExt.ColorSchemesView())
{
for (const auto&& entry : fragExt.ColorSchemesView())
{
for (const auto& schemeVM : _colorSchemesPageVM.AllColorSchemes())
{
if (schemeVM.Name() == entry.ColorSchemeName())
{
auto vm = winrt::make<FragmentColorSchemeViewModel>(entry, fragExt, schemeVM);
currentColorSchemesAdded.push_back(vm);
if (extensionEnabled)
{
colorSchemesAddedTotal.push_back(vm);
}
}
}
}
}
extPkgVM->FragmentExtensions().Append(winrt::make<FragmentExtensionViewModel>(fragExt, currentProfilesModified, currentProfilesAdded, currentColorSchemesAdded));
}
}

_extensionPackages = single_threaded_observable_vector<Editor::ExtensionPackageViewModel>(std::move(extensionPackages));
_profilesModifiedView = single_threaded_observable_vector<Editor::FragmentProfileViewModel>(std::move(profilesModifiedTotal));
_profilesAddedView = single_threaded_observable_vector<Editor::FragmentProfileViewModel>(std::move(profilesAddedTotal));
_colorSchemesAddedView = single_threaded_observable_vector<Editor::FragmentColorSchemeViewModel>(std::move(colorSchemesAddedTotal));
}

Windows::UI::Xaml::DataTemplate ExtensionsViewModel::CurrentExtensionPackageIdentifierTemplate() const
{
return _ExtensionPackageIdentifierTemplateSelector.SelectTemplate(CurrentExtensionPackage());
}

// Returns true if the extension is enabled, false otherwise
bool ExtensionsViewModel::GetExtensionState(hstring extensionSource, const Model::CascadiaSettings& settings)
{
if (const auto& disabledExtensions = settings.GlobalSettings().DisabledProfileSources())
{
uint32_t ignored;
return !disabledExtensions.IndexOf(extensionSource, ignored);
}
// "disabledProfileSources" not defined --> all extensions are enabled
return true;
}

// Enable/Disable an extension
void ExtensionsViewModel::SetExtensionState(hstring extensionSource, const Model::CascadiaSettings& settings, bool enableExt)
{
// get the current status of the extension
uint32_t idx;
bool currentlyEnabled = true;
const auto& disabledExtensions = settings.GlobalSettings().DisabledProfileSources();
if (disabledExtensions)
{
currentlyEnabled = !disabledExtensions.IndexOf(extensionSource, idx);
}

// current status mismatches the desired status,
// update the list of disabled extensions
if (currentlyEnabled != enableExt)
{
// If we're disabling an extension and we don't have "disabledProfileSources" defined,
// create it in the model directly
if (!disabledExtensions && !enableExt)
{
std::vector<hstring> disabledProfileSources{ extensionSource };
settings.GlobalSettings().DisabledProfileSources(single_threaded_vector<hstring>(std::move(disabledProfileSources)));
return;
}

// Update the list of disabled extensions
if (enableExt)
{
disabledExtensions.RemoveAt(idx);
}
else
{
disabledExtensions.Append(extensionSource);
}
}
}

void ExtensionsViewModel::NavigateToProfile(const guid profileGuid)
{
NavigateToProfileRequested.raise(*this, profileGuid);
}

void ExtensionsViewModel::NavigateToColorScheme(const Editor::ColorSchemeViewModel& schemeVM)
{
_colorSchemesPageVM.CurrentScheme(schemeVM);
NavigateToColorSchemeRequested.raise(*this, nullptr);
}

hstring ExtensionPackageViewModel::Scope() const noexcept
{
return _package.Scope() == Model::FragmentScope::User ? RS_(L"Extensions_ScopeUser") : RS_(L"Extensions_ScopeSystem");
}

bool ExtensionPackageViewModel::Enabled() const
{
return ExtensionsViewModel::GetExtensionState(_package.Source(), _settings);
}

void ExtensionPackageViewModel::Enabled(bool val)
{
if (Enabled() != val)
{
ExtensionsViewModel::SetExtensionState(_package.Source(), _settings, val);
_NotifyChanges(L"Enabled");
}
}

// Returns the accessible name for the extension package in the following format:
// "<DisplayName?>, <Source>"
hstring ExtensionPackageViewModel::AccessibleName() const noexcept
{
hstring name;
const auto source = _package.Source();
if (const auto displayName = _package.DisplayName(); !displayName.empty())
{
return hstring{ fmt::format(FMT_COMPILE(L"{}, {}"), displayName, source) };
}
return source;
}

// Returns the accessible name for the extension package with the disabled state (if disabled) in the following format:
// "<DisplayName?>, <Source>: <Disabled?>"
hstring ExtensionPackageViewModel::AccessibleNameWithStatus() const noexcept
{
if (Enabled())
{
return AccessibleName();
}
return hstring{ fmt::format(FMT_COMPILE(L"{}: {}"), AccessibleName(), RS_(L"Extension_StateDisabled/Text")) };
}

hstring FragmentProfileViewModel::AccessibleName() const noexcept
{
return hstring{ fmt::format(FMT_COMPILE(L"{}, {}"), Profile().Name(), SourceName()) };
}

hstring FragmentColorSchemeViewModel::AccessibleName() const noexcept
{
return hstring{ fmt::format(FMT_COMPILE(L"{}, {}"), ColorSchemeVM().Name(), SourceName()) };
}

DataTemplate ExtensionPackageTemplateSelector::SelectTemplateCore(const IInspectable& item, const DependencyObject& /*container*/)
{
return SelectTemplateCore(item);
}

DataTemplate ExtensionPackageTemplateSelector::SelectTemplateCore(const IInspectable& item)
{
if (const auto extPkgVM = item.try_as<Editor::ExtensionPackageViewModel>())
{
if (!extPkgVM.Package().DisplayName().empty())
{
return ComplexTemplate();
}
return DefaultTemplate();
}
return nullptr;
}
}
Loading
Loading