mirror of
https://github.com/microsoft/terminal.git
synced 2026-04-12 01:01:05 +00:00
Compare commits
22 Commits
dev/lhecke
...
dev/cazamo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0fc5627032 | ||
|
|
52a2b49336 | ||
|
|
869a1915d0 | ||
|
|
05ded928fb | ||
|
|
3f31d56719 | ||
|
|
172893bdfa | ||
|
|
5c34eb73e2 | ||
|
|
71be4eec34 | ||
|
|
68dc291ff4 | ||
|
|
f650f45002 | ||
|
|
62b27a664e | ||
|
|
cb04e7fb47 | ||
|
|
6932db03e9 | ||
|
|
27008bf4bd | ||
|
|
7cdbb7c795 | ||
|
|
5afc9bc86a | ||
|
|
6b83fa705a | ||
|
|
e6c43c0d4c | ||
|
|
75d02c29bd | ||
|
|
f11515e692 | ||
|
|
7f0c9e5374 | ||
|
|
019bb766db |
Binary file not shown.
|
After Width: | Height: | Size: 943 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src/cascadia/CascadiaPackage/ProfileGeneratorIcons/SSH.png
Normal file
BIN
src/cascadia/CascadiaPackage/ProfileGeneratorIcons/SSH.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 787 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
BIN
src/cascadia/CascadiaPackage/ProfileGeneratorIcons/WSL.png
Normal file
BIN
src/cascadia/CascadiaPackage/ProfileGeneratorIcons/WSL.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
@@ -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>
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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}" />
|
||||
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
342
src/cascadia/TerminalSettingsEditor/Extensions.cpp
Normal file
342
src/cascadia/TerminalSettingsEditor/Extensions.cpp
Normal file
@@ -0,0 +1,342 @@
|
||||
// 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
|
||||
{
|
||||
static constexpr std::wstring_view ExtensionPageId{ L"page.extensions" };
|
||||
|
||||
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>();
|
||||
auto vmImpl = get_self<ExtensionsViewModel>(_ViewModel);
|
||||
vmImpl->ExtensionPackageIdentifierTemplateSelector(_extensionPackageIdentifierTemplateSelector);
|
||||
vmImpl->MarkAsVisited();
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
bool ExtensionsViewModel::DisplayBadge() const noexcept
|
||||
{
|
||||
return !Model::ApplicationState::SharedInstance().BadgeDismissed(ExtensionPageId);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
void ExtensionsViewModel::MarkAsVisited()
|
||||
{
|
||||
Model::ApplicationState::SharedInstance().DismissBadge(ExtensionPageId);
|
||||
_NotifyChanges(L"DisplayBadge");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
180
src/cascadia/TerminalSettingsEditor/Extensions.h
Normal file
180
src/cascadia/TerminalSettingsEditor/Extensions.h
Normal file
@@ -0,0 +1,180 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Extensions.g.h"
|
||||
#include "ExtensionsViewModel.g.h"
|
||||
#include "ExtensionPackageViewModel.g.h"
|
||||
#include "FragmentExtensionViewModel.g.h"
|
||||
#include "FragmentProfileViewModel.g.h"
|
||||
#include "FragmentColorSchemeViewModel.g.h"
|
||||
#include "ExtensionPackageTemplateSelector.g.h"
|
||||
#include "ViewModelHelpers.h"
|
||||
#include "Utils.h"
|
||||
|
||||
namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
|
||||
{
|
||||
struct Extensions : public HasScrollViewer<Extensions>, ExtensionsT<Extensions>
|
||||
{
|
||||
public:
|
||||
Extensions();
|
||||
|
||||
void OnNavigatedTo(const Windows::UI::Xaml::Navigation::NavigationEventArgs& e);
|
||||
|
||||
void ExtensionNavigator_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& args);
|
||||
void NavigateToProfile_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& args);
|
||||
void NavigateToColorScheme_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& args);
|
||||
|
||||
WINRT_PROPERTY(Editor::ExtensionsViewModel, ViewModel, nullptr);
|
||||
|
||||
private:
|
||||
Editor::ExtensionPackageTemplateSelector _extensionPackageIdentifierTemplateSelector;
|
||||
};
|
||||
|
||||
struct ExtensionsViewModel : ExtensionsViewModelT<ExtensionsViewModel>, ViewModelHelper<ExtensionsViewModel>
|
||||
{
|
||||
public:
|
||||
ExtensionsViewModel(const Model::CascadiaSettings& settings, const Editor::ColorSchemesPageViewModel& colorSchemesPageVM);
|
||||
|
||||
// Properties
|
||||
Windows::UI::Xaml::DataTemplate CurrentExtensionPackageIdentifierTemplate() const;
|
||||
bool IsExtensionView() const noexcept { return _CurrentExtensionPackage != nullptr; }
|
||||
bool NoExtensionPackages() const noexcept { return _extensionPackages.Size() == 0; }
|
||||
bool NoProfilesModified() const noexcept { return _profilesModifiedView.Size() == 0; }
|
||||
bool NoProfilesAdded() const noexcept { return _profilesAddedView.Size() == 0; }
|
||||
bool NoSchemesAdded() const noexcept { return _colorSchemesAddedView.Size() == 0; }
|
||||
bool DisplayBadge() const noexcept;
|
||||
|
||||
// Views
|
||||
Windows::Foundation::Collections::IObservableVector<Editor::ExtensionPackageViewModel> ExtensionPackages() const noexcept { return _extensionPackages; }
|
||||
Windows::Foundation::Collections::IObservableVector<Editor::FragmentProfileViewModel> ProfilesModified() const noexcept { return _profilesModifiedView; }
|
||||
Windows::Foundation::Collections::IObservableVector<Editor::FragmentProfileViewModel> ProfilesAdded() const noexcept { return _profilesAddedView; }
|
||||
Windows::Foundation::Collections::IObservableVector<Editor::FragmentColorSchemeViewModel> ColorSchemesAdded() const noexcept { return _colorSchemesAddedView; }
|
||||
|
||||
// Methods
|
||||
void UpdateSettings(const Model::CascadiaSettings& settings, const Editor::ColorSchemesPageViewModel& colorSchemesPageVM);
|
||||
void NavigateToProfile(const guid profileGuid);
|
||||
void NavigateToColorScheme(const Editor::ColorSchemeViewModel& schemeVM);
|
||||
void MarkAsVisited();
|
||||
|
||||
static bool GetExtensionState(hstring extensionSource, const Model::CascadiaSettings& settings);
|
||||
static void SetExtensionState(hstring extensionSource, const Model::CascadiaSettings& settings, bool enableExt);
|
||||
|
||||
til::typed_event<IInspectable, guid> NavigateToProfileRequested;
|
||||
til::typed_event<IInspectable, Editor::ColorSchemeViewModel> NavigateToColorSchemeRequested;
|
||||
|
||||
VIEW_MODEL_OBSERVABLE_PROPERTY(Editor::ExtensionPackageViewModel, CurrentExtensionPackage, nullptr);
|
||||
WINRT_PROPERTY(Editor::ExtensionPackageTemplateSelector, ExtensionPackageIdentifierTemplateSelector, nullptr);
|
||||
|
||||
private:
|
||||
Model::CascadiaSettings _settings;
|
||||
Editor::ColorSchemesPageViewModel _colorSchemesPageVM;
|
||||
Windows::Foundation::Collections::IObservableVector<Editor::ExtensionPackageViewModel> _extensionPackages;
|
||||
Windows::Foundation::Collections::IObservableVector<Editor::FragmentProfileViewModel> _profilesModifiedView;
|
||||
Windows::Foundation::Collections::IObservableVector<Editor::FragmentProfileViewModel> _profilesAddedView;
|
||||
Windows::Foundation::Collections::IObservableVector<Editor::FragmentColorSchemeViewModel> _colorSchemesAddedView;
|
||||
};
|
||||
|
||||
struct ExtensionPackageViewModel : ExtensionPackageViewModelT<ExtensionPackageViewModel>, ViewModelHelper<ExtensionPackageViewModel>
|
||||
{
|
||||
public:
|
||||
ExtensionPackageViewModel(const Model::ExtensionPackage& pkg, const Model::CascadiaSettings& settings) :
|
||||
_package{ pkg },
|
||||
_settings{ settings },
|
||||
_fragmentExtensions{ single_threaded_observable_vector<Editor::FragmentExtensionViewModel>() } {}
|
||||
|
||||
Model::ExtensionPackage Package() const noexcept { return _package; }
|
||||
hstring Scope() const noexcept;
|
||||
bool Enabled() const;
|
||||
void Enabled(bool val);
|
||||
hstring AccessibleName() const noexcept;
|
||||
hstring AccessibleNameWithStatus() const noexcept;
|
||||
Windows::Foundation::Collections::IObservableVector<Editor::FragmentExtensionViewModel> FragmentExtensions() { return _fragmentExtensions; }
|
||||
|
||||
private:
|
||||
Model::ExtensionPackage _package;
|
||||
Model::CascadiaSettings _settings;
|
||||
Windows::Foundation::Collections::IObservableVector<Editor::FragmentExtensionViewModel> _fragmentExtensions;
|
||||
};
|
||||
|
||||
struct FragmentExtensionViewModel : FragmentExtensionViewModelT<FragmentExtensionViewModel>, ViewModelHelper<FragmentExtensionViewModel>
|
||||
{
|
||||
public:
|
||||
FragmentExtensionViewModel(const Model::FragmentSettings& fragment,
|
||||
std::vector<FragmentProfileViewModel>& profilesModified,
|
||||
std::vector<FragmentProfileViewModel>& profilesAdded,
|
||||
std::vector<FragmentColorSchemeViewModel>& colorSchemesAdded) :
|
||||
_fragment{ fragment },
|
||||
_profilesModified{ single_threaded_vector(std::move(profilesModified)) },
|
||||
_profilesAdded{ single_threaded_vector(std::move(profilesAdded)) },
|
||||
_colorSchemesAdded{ single_threaded_vector(std::move(colorSchemesAdded)) } {}
|
||||
|
||||
Model::FragmentSettings Fragment() const noexcept { return _fragment; }
|
||||
Windows::Foundation::Collections::IVectorView<FragmentProfileViewModel> ProfilesModified() const noexcept { return _profilesModified.GetView(); }
|
||||
Windows::Foundation::Collections::IVectorView<FragmentProfileViewModel> ProfilesAdded() const noexcept { return _profilesAdded.GetView(); }
|
||||
Windows::Foundation::Collections::IVectorView<FragmentColorSchemeViewModel> ColorSchemesAdded() const noexcept { return _colorSchemesAdded.GetView(); }
|
||||
|
||||
private:
|
||||
Model::FragmentSettings _fragment;
|
||||
Windows::Foundation::Collections::IVector<FragmentProfileViewModel> _profilesModified;
|
||||
Windows::Foundation::Collections::IVector<FragmentProfileViewModel> _profilesAdded;
|
||||
Windows::Foundation::Collections::IVector<FragmentColorSchemeViewModel> _colorSchemesAdded;
|
||||
};
|
||||
|
||||
struct FragmentProfileViewModel : FragmentProfileViewModelT<FragmentProfileViewModel>, ViewModelHelper<FragmentProfileViewModel>
|
||||
{
|
||||
public:
|
||||
FragmentProfileViewModel(const Model::FragmentProfileEntry& entry, const Model::FragmentSettings& fragment, const Model::Profile& deducedProfile) :
|
||||
_entry{ entry },
|
||||
_fragment{ fragment },
|
||||
_deducedProfile{ deducedProfile } {}
|
||||
|
||||
Model::Profile Profile() const { return _deducedProfile; };
|
||||
hstring SourceName() const { return _fragment.Source(); }
|
||||
hstring Json() const { return _entry.Json(); }
|
||||
hstring AccessibleName() const noexcept;
|
||||
|
||||
private:
|
||||
Model::FragmentProfileEntry _entry;
|
||||
Model::FragmentSettings _fragment;
|
||||
Model::Profile _deducedProfile;
|
||||
};
|
||||
|
||||
struct FragmentColorSchemeViewModel : FragmentColorSchemeViewModelT<FragmentColorSchemeViewModel>, ViewModelHelper<FragmentColorSchemeViewModel>
|
||||
{
|
||||
public:
|
||||
FragmentColorSchemeViewModel(const Model::FragmentColorSchemeEntry& entry, const Model::FragmentSettings& fragment, const Editor::ColorSchemeViewModel& deducedSchemeVM) :
|
||||
_entry{ entry },
|
||||
_fragment{ fragment },
|
||||
_deducedSchemeVM{ deducedSchemeVM } {}
|
||||
|
||||
Editor::ColorSchemeViewModel ColorSchemeVM() const { return _deducedSchemeVM; };
|
||||
hstring SourceName() const { return _fragment.Source(); }
|
||||
hstring Json() const { return _entry.Json(); }
|
||||
hstring AccessibleName() const noexcept;
|
||||
|
||||
private:
|
||||
Model::FragmentColorSchemeEntry _entry;
|
||||
Model::FragmentSettings _fragment;
|
||||
Editor::ColorSchemeViewModel _deducedSchemeVM;
|
||||
};
|
||||
|
||||
struct ExtensionPackageTemplateSelector : public ExtensionPackageTemplateSelectorT<ExtensionPackageTemplateSelector>
|
||||
{
|
||||
public:
|
||||
ExtensionPackageTemplateSelector() = default;
|
||||
|
||||
Windows::UI::Xaml::DataTemplate SelectTemplateCore(const Windows::Foundation::IInspectable& item, const Windows::UI::Xaml::DependencyObject& container);
|
||||
Windows::UI::Xaml::DataTemplate SelectTemplateCore(const Windows::Foundation::IInspectable& item);
|
||||
|
||||
WINRT_PROPERTY(Windows::UI::Xaml::DataTemplate, DefaultTemplate, nullptr);
|
||||
WINRT_PROPERTY(Windows::UI::Xaml::DataTemplate, ComplexTemplate, nullptr);
|
||||
};
|
||||
};
|
||||
|
||||
namespace winrt::Microsoft::Terminal::Settings::Editor::factory_implementation
|
||||
{
|
||||
BASIC_FACTORY(Extensions);
|
||||
BASIC_FACTORY(ExtensionPackageTemplateSelector);
|
||||
}
|
||||
80
src/cascadia/TerminalSettingsEditor/Extensions.idl
Normal file
80
src/cascadia/TerminalSettingsEditor/Extensions.idl
Normal file
@@ -0,0 +1,80 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import "ColorSchemesPageViewModel.idl";
|
||||
|
||||
namespace Microsoft.Terminal.Settings.Editor
|
||||
{
|
||||
[default_interface] runtimeclass Extensions : Windows.UI.Xaml.Controls.Page
|
||||
{
|
||||
Extensions();
|
||||
ExtensionsViewModel ViewModel { get; };
|
||||
}
|
||||
|
||||
[default_interface] runtimeclass ExtensionsViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged
|
||||
{
|
||||
// Properties
|
||||
ExtensionPackageViewModel CurrentExtensionPackage;
|
||||
Windows.UI.Xaml.DataTemplate CurrentExtensionPackageIdentifierTemplate { get; };
|
||||
Boolean IsExtensionView { get; };
|
||||
Boolean NoExtensionPackages { get; };
|
||||
Boolean NoProfilesModified { get; };
|
||||
Boolean NoProfilesAdded { get; };
|
||||
Boolean NoSchemesAdded { get; };
|
||||
Boolean DisplayBadge { get; };
|
||||
|
||||
// Views
|
||||
IVector<ExtensionPackageViewModel> ExtensionPackages { get; };
|
||||
IObservableVector<FragmentProfileViewModel> ProfilesModified { get; };
|
||||
IObservableVector<FragmentProfileViewModel> ProfilesAdded { get; };
|
||||
IObservableVector<FragmentColorSchemeViewModel> ColorSchemesAdded { get; };
|
||||
|
||||
// Methods
|
||||
void UpdateSettings(Microsoft.Terminal.Settings.Model.CascadiaSettings settings, ColorSchemesPageViewModel colorSchemesPageVM);
|
||||
|
||||
event Windows.Foundation.TypedEventHandler<Object, Guid> NavigateToProfileRequested;
|
||||
event Windows.Foundation.TypedEventHandler<Object, ColorSchemeViewModel> NavigateToColorSchemeRequested;
|
||||
}
|
||||
|
||||
[default_interface] runtimeclass ExtensionPackageViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged
|
||||
{
|
||||
Microsoft.Terminal.Settings.Model.ExtensionPackage Package { get; };
|
||||
Boolean Enabled;
|
||||
String Scope { get; };
|
||||
String AccessibleName { get; };
|
||||
String AccessibleNameWithStatus { get; };
|
||||
IVector<FragmentExtensionViewModel> FragmentExtensions { get; };
|
||||
}
|
||||
|
||||
[default_interface] runtimeclass FragmentExtensionViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged
|
||||
{
|
||||
Microsoft.Terminal.Settings.Model.FragmentSettings Fragment { get; };
|
||||
IVectorView<FragmentProfileViewModel> ProfilesModified { get; };
|
||||
IVectorView<FragmentProfileViewModel> ProfilesAdded { get; };
|
||||
IVectorView<FragmentColorSchemeViewModel> ColorSchemesAdded { get; };
|
||||
}
|
||||
|
||||
[default_interface] runtimeclass FragmentProfileViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged
|
||||
{
|
||||
Microsoft.Terminal.Settings.Model.Profile Profile { get; };
|
||||
String SourceName { get; };
|
||||
String Json { get; };
|
||||
String AccessibleName { get; };
|
||||
}
|
||||
|
||||
[default_interface] runtimeclass FragmentColorSchemeViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged
|
||||
{
|
||||
ColorSchemeViewModel ColorSchemeVM { get; };
|
||||
String SourceName { get; };
|
||||
String Json { get; };
|
||||
String AccessibleName { get; };
|
||||
}
|
||||
|
||||
[default_interface] runtimeclass ExtensionPackageTemplateSelector : Windows.UI.Xaml.Controls.DataTemplateSelector
|
||||
{
|
||||
ExtensionPackageTemplateSelector();
|
||||
|
||||
Windows.UI.Xaml.DataTemplate DefaultTemplate;
|
||||
Windows.UI.Xaml.DataTemplate ComplexTemplate;
|
||||
}
|
||||
}
|
||||
494
src/cascadia/TerminalSettingsEditor/Extensions.xaml
Normal file
494
src/cascadia/TerminalSettingsEditor/Extensions.xaml
Normal file
@@ -0,0 +1,494 @@
|
||||
<!--
|
||||
Copyright (c) Microsoft Corporation. All rights reserved. Licensed under
|
||||
the MIT License. See LICENSE in the project root for license information.
|
||||
-->
|
||||
<Page x:Class="Microsoft.Terminal.Settings.Editor.Extensions"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="using:Microsoft.Terminal.Settings.Editor"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:model="using:Microsoft.Terminal.Settings.Model"
|
||||
xmlns:mtu="using:Microsoft.Terminal.UI"
|
||||
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Page.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="CommonResources.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
<Style x:Key="ItalicDisclaimerStyle"
|
||||
BasedOn="{StaticResource DisclaimerStyle}"
|
||||
TargetType="TextBlock">
|
||||
<Setter Property="FontStyle" Value="Italic" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="CodeBlockStyle"
|
||||
TargetType="TextBlock">
|
||||
<Setter Property="FontFamily" Value="Cascadia Mono, Consolas" />
|
||||
<Setter Property="IsTextSelectionEnabled" Value="True" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="CodeBlockScrollViewerStyle"
|
||||
TargetType="ScrollViewer">
|
||||
<Setter Property="AutomationProperties.AccessibilityView" Value="Raw" />
|
||||
<Setter Property="HorizontalScrollMode" Value="Auto" />
|
||||
<Setter Property="HorizontalScrollBarVisibility" Value="Auto" />
|
||||
<Setter Property="VerticalScrollMode" Value="Disabled" />
|
||||
<Setter Property="VerticalScrollBarVisibility" Value="Disabled" />
|
||||
</Style>
|
||||
|
||||
<local:ExtensionPackageTemplateSelector x:Key="ExtensionPackageIdentifierTemplateSelector"
|
||||
ComplexTemplate="{StaticResource ComplexExtensionIdentifierTemplate}"
|
||||
DefaultTemplate="{StaticResource DefaultExtensionIdentifierTemplate}" />
|
||||
|
||||
<DataTemplate x:Key="DefaultExtensionIdentifierTemplate"
|
||||
x:DataType="local:ExtensionPackageViewModel">
|
||||
<TextBlock Text="{x:Bind Package.Source}" />
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="ComplexExtensionIdentifierTemplate"
|
||||
x:DataType="local:ExtensionPackageViewModel">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<IconSourceElement Grid.Row="0"
|
||||
Grid.RowSpan="2"
|
||||
Grid.Column="0"
|
||||
Width="32"
|
||||
Height="32"
|
||||
Margin="0,0,8,0"
|
||||
IconSource="{x:Bind mtu:IconPathConverter.IconSourceWUX(Package.Icon)}" />
|
||||
<TextBlock Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Text="{x:Bind Package.DisplayName}" />
|
||||
<TextBlock Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Style="{StaticResource SettingsPageItemDescriptionStyle}"
|
||||
Text="{x:Bind Package.Source}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<local:ExtensionPackageTemplateSelector x:Key="ExtensionPackageNavigatorTemplateSelector"
|
||||
ComplexTemplate="{StaticResource ComplexExtensionNavigatorTemplate}"
|
||||
DefaultTemplate="{StaticResource DefaultExtensionNavigatorTemplate}" />
|
||||
|
||||
<DataTemplate x:Key="DefaultExtensionNavigatorTemplate"
|
||||
x:DataType="local:ExtensionPackageViewModel">
|
||||
<Button AutomationProperties.Name="{x:Bind AccessibleName}"
|
||||
Click="ExtensionNavigator_Click"
|
||||
Style="{StaticResource NavigatorButtonStyle}"
|
||||
Tag="{x:Bind}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<ContentPresenter Content="{x:Bind}"
|
||||
ContentTemplate="{StaticResource DefaultExtensionIdentifierTemplate}" />
|
||||
|
||||
<TextBlock x:Uid="Extension_StateDisabled"
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource SecondaryTextBlockStyle}"
|
||||
Visibility="{x:Bind mtu:Converters.InvertedBooleanToVisibility(Enabled)}" />
|
||||
</Grid>
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="ComplexExtensionNavigatorTemplate"
|
||||
x:DataType="local:ExtensionPackageViewModel">
|
||||
<Button AutomationProperties.Name="{x:Bind AccessibleNameWithStatus}"
|
||||
Click="ExtensionNavigator_Click"
|
||||
Style="{StaticResource NavigatorButtonStyle}"
|
||||
Tag="{x:Bind}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<ContentPresenter Content="{x:Bind}"
|
||||
ContentTemplate="{StaticResource ComplexExtensionIdentifierTemplate}" />
|
||||
|
||||
<TextBlock x:Uid="Extension_StateDisabled"
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource SecondaryTextBlockStyle}"
|
||||
Visibility="{x:Bind mtu:Converters.InvertedBooleanToVisibility(Enabled)}" />
|
||||
</Grid>
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="FragmentProfileViewModelTemplate"
|
||||
x:DataType="local:FragmentProfileViewModel">
|
||||
<muxc:Expander AutomationProperties.Name="{x:Bind AccessibleName}"
|
||||
Style="{StaticResource ExpanderStyle}">
|
||||
<muxc:Expander.Header>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel Grid.Column="0"
|
||||
Orientation="Horizontal">
|
||||
<IconSourceElement Width="16"
|
||||
Height="16"
|
||||
Margin="0,0,8,0"
|
||||
IconSource="{x:Bind mtu:IconPathConverter.IconSourceWUX(Profile.EvaluatedIcon), Mode=OneWay}" />
|
||||
|
||||
<TextBlock Text="{x:Bind Profile.Name, Mode=OneWay}" />
|
||||
|
||||
<Button x:Name="NavigateToProfileButton"
|
||||
x:Uid="Extensions_NavigateToProfileButton"
|
||||
Click="NavigateToProfile_Click"
|
||||
Style="{StaticResource SettingContainerResetButtonStyle}"
|
||||
Tag="{x:Bind Profile.Guid}"
|
||||
Visibility="{x:Bind mtu:Converters.InvertedBooleanToVisibility(Profile.Deleted)}">
|
||||
<FontIcon Glyph=""
|
||||
Style="{StaticResource SettingContainerFontIconStyle}" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Grid.Column="1"
|
||||
Style="{StaticResource SettingContainerCurrentValueTextBlockStyle}"
|
||||
Text="{x:Bind SourceName}" />
|
||||
</Grid>
|
||||
</muxc:Expander.Header>
|
||||
<muxc:Expander.Content>
|
||||
<ScrollViewer Style="{StaticResource CodeBlockScrollViewerStyle}">
|
||||
<TextBlock Style="{StaticResource CodeBlockStyle}"
|
||||
Text="{x:Bind Json, Mode=OneWay}" />
|
||||
</ScrollViewer>
|
||||
</muxc:Expander.Content>
|
||||
</muxc:Expander>
|
||||
</DataTemplate>
|
||||
|
||||
<!-- This styling matches that of ExpanderSettingContainerStyle for consistency -->
|
||||
<Style x:Key="ExpanderStyle"
|
||||
TargetType="muxc:Expander">
|
||||
<Setter Property="MaxWidth" Value="1000" />
|
||||
<Setter Property="MinHeight" Value="64" />
|
||||
<Setter Property="Margin" Value="0,4,0,0" />
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
</Style>
|
||||
|
||||
<DataTemplate x:Key="JsonTemplate"
|
||||
x:DataType="local:FragmentExtensionViewModel">
|
||||
<muxc:Expander Header="{x:Bind Fragment.JsonSource}"
|
||||
Style="{StaticResource ExpanderStyle}">
|
||||
<ScrollViewer Style="{StaticResource CodeBlockScrollViewerStyle}">
|
||||
<TextBlock Style="{StaticResource CodeBlockStyle}"
|
||||
Text="{x:Bind Fragment.Json}" />
|
||||
</ScrollViewer>
|
||||
</muxc:Expander>
|
||||
</DataTemplate>
|
||||
|
||||
<!--
|
||||
Copied over from Appearances.xaml. We're unable to add the DataTemplate to CommonResources.xaml
|
||||
because it needs a code-behind class to use {x:Bind}
|
||||
-->
|
||||
<DataTemplate x:Key="ColorChipTemplate"
|
||||
x:DataType="local:ColorTableEntry">
|
||||
<Border Width="8"
|
||||
Height="8"
|
||||
Background="{x:Bind mtu:Converters.ColorToBrush(Color)}"
|
||||
CornerRadius="1" />
|
||||
</DataTemplate>
|
||||
|
||||
<!--
|
||||
Copied over from Appearances.xaml. We're unable to add the DataTemplate to CommonResources.xaml
|
||||
because it needs a code-behind class to use {x:Bind}
|
||||
-->
|
||||
<DataTemplate x:Key="ColorSchemeVMTemplate"
|
||||
x:DataType="local:ColorSchemeViewModel">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Grid Grid.Column="0"
|
||||
Padding="8"
|
||||
VerticalAlignment="Center"
|
||||
Background="{x:Bind mtu:Converters.ColorToBrush(BackgroundColor.Color), Mode=OneWay}"
|
||||
ColumnSpacing="1"
|
||||
CornerRadius="2"
|
||||
RowSpacing="1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<ContentControl Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Content="{x:Bind ColorEntryAt(0), Mode=OneWay}"
|
||||
ContentTemplate="{StaticResource ColorChipTemplate}"
|
||||
IsTabStop="False" />
|
||||
<ContentControl Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Content="{x:Bind ColorEntryAt(1), Mode=OneWay}"
|
||||
ContentTemplate="{StaticResource ColorChipTemplate}"
|
||||
IsTabStop="False" />
|
||||
<ContentControl Grid.Row="0"
|
||||
Grid.Column="2"
|
||||
Content="{x:Bind ColorEntryAt(2), Mode=OneWay}"
|
||||
ContentTemplate="{StaticResource ColorChipTemplate}"
|
||||
IsTabStop="False" />
|
||||
<ContentControl Grid.Row="0"
|
||||
Grid.Column="3"
|
||||
Content="{x:Bind ColorEntryAt(3), Mode=OneWay}"
|
||||
ContentTemplate="{StaticResource ColorChipTemplate}"
|
||||
IsTabStop="False" />
|
||||
<ContentControl Grid.Row="0"
|
||||
Grid.Column="4"
|
||||
Content="{x:Bind ColorEntryAt(4), Mode=OneWay}"
|
||||
ContentTemplate="{StaticResource ColorChipTemplate}"
|
||||
IsTabStop="False" />
|
||||
<ContentControl Grid.Row="0"
|
||||
Grid.Column="5"
|
||||
Content="{x:Bind ColorEntryAt(5), Mode=OneWay}"
|
||||
ContentTemplate="{StaticResource ColorChipTemplate}"
|
||||
IsTabStop="False" />
|
||||
<ContentControl Grid.Row="0"
|
||||
Grid.Column="6"
|
||||
Content="{x:Bind ColorEntryAt(6), Mode=OneWay}"
|
||||
ContentTemplate="{StaticResource ColorChipTemplate}"
|
||||
IsTabStop="False" />
|
||||
<ContentControl Grid.Row="0"
|
||||
Grid.Column="7"
|
||||
Content="{x:Bind ColorEntryAt(7), Mode=OneWay}"
|
||||
ContentTemplate="{StaticResource ColorChipTemplate}"
|
||||
IsTabStop="False" />
|
||||
<ContentControl Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Content="{x:Bind ColorEntryAt(8), Mode=OneWay}"
|
||||
ContentTemplate="{StaticResource ColorChipTemplate}"
|
||||
IsTabStop="False" />
|
||||
<ContentControl Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Content="{x:Bind ColorEntryAt(9), Mode=OneWay}"
|
||||
ContentTemplate="{StaticResource ColorChipTemplate}"
|
||||
IsTabStop="False" />
|
||||
<ContentControl Grid.Row="1"
|
||||
Grid.Column="2"
|
||||
Content="{x:Bind ColorEntryAt(10), Mode=OneWay}"
|
||||
ContentTemplate="{StaticResource ColorChipTemplate}"
|
||||
IsTabStop="False" />
|
||||
<ContentControl Grid.Row="1"
|
||||
Grid.Column="3"
|
||||
Content="{x:Bind ColorEntryAt(11), Mode=OneWay}"
|
||||
ContentTemplate="{StaticResource ColorChipTemplate}"
|
||||
IsTabStop="False" />
|
||||
<ContentControl Grid.Row="1"
|
||||
Grid.Column="4"
|
||||
Content="{x:Bind ColorEntryAt(12), Mode=OneWay}"
|
||||
ContentTemplate="{StaticResource ColorChipTemplate}"
|
||||
IsTabStop="False" />
|
||||
<ContentControl Grid.Row="1"
|
||||
Grid.Column="5"
|
||||
Content="{x:Bind ColorEntryAt(13), Mode=OneWay}"
|
||||
ContentTemplate="{StaticResource ColorChipTemplate}"
|
||||
IsTabStop="False" />
|
||||
<ContentControl Grid.Row="1"
|
||||
Grid.Column="6"
|
||||
Content="{x:Bind ColorEntryAt(14), Mode=OneWay}"
|
||||
ContentTemplate="{StaticResource ColorChipTemplate}"
|
||||
IsTabStop="False" />
|
||||
<ContentControl Grid.Row="1"
|
||||
Grid.Column="7"
|
||||
Content="{x:Bind ColorEntryAt(15), Mode=OneWay}"
|
||||
ContentTemplate="{StaticResource ColorChipTemplate}"
|
||||
IsTabStop="False" />
|
||||
<TextBlock Grid.RowSpan="2"
|
||||
Grid.Column="8"
|
||||
MaxWidth="192"
|
||||
Margin="4,0,4,0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
FontFamily="Cascadia Code"
|
||||
Foreground="{x:Bind mtu:Converters.ColorToBrush(ForegroundColor.Color), Mode=OneWay}"
|
||||
Text="{x:Bind Name, Mode=OneWay}"
|
||||
TextTrimming="WordEllipsis" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="FragmentColorSchemeViewModelTemplate"
|
||||
x:DataType="local:FragmentColorSchemeViewModel">
|
||||
<muxc:Expander AutomationProperties.Name="{x:Bind AccessibleName}"
|
||||
Style="{StaticResource ExpanderStyle}">
|
||||
<muxc:Expander.Header>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel Grid.Column="0"
|
||||
Orientation="Horizontal">
|
||||
<ContentPresenter Content="{x:Bind ColorSchemeVM, Mode=OneWay}"
|
||||
ContentTemplate="{StaticResource ColorSchemeVMTemplate}" />
|
||||
<Button x:Name="NavigateToColorSchemeButton"
|
||||
x:Uid="Extensions_NavigateToColorSchemeButton"
|
||||
Click="NavigateToColorScheme_Click"
|
||||
Style="{StaticResource SettingContainerResetButtonStyle}"
|
||||
Tag="{x:Bind ColorSchemeVM}">
|
||||
<FontIcon Glyph=""
|
||||
Style="{StaticResource SettingContainerFontIconStyle}" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Grid.Column="1"
|
||||
Style="{StaticResource SettingContainerCurrentValueTextBlockStyle}"
|
||||
Text="{x:Bind SourceName}" />
|
||||
</Grid>
|
||||
</muxc:Expander.Header>
|
||||
<muxc:Expander.Content>
|
||||
<ScrollViewer Style="{StaticResource CodeBlockScrollViewerStyle}">
|
||||
<TextBlock Style="{StaticResource CodeBlockStyle}"
|
||||
Text="{x:Bind Json, Mode=OneWay}" />
|
||||
</ScrollViewer>
|
||||
</muxc:Expander.Content>
|
||||
</muxc:Expander>
|
||||
</DataTemplate>
|
||||
</ResourceDictionary>
|
||||
</Page.Resources>
|
||||
|
||||
<StackPanel Spacing="20"
|
||||
Style="{StaticResource SettingsStackStyle}">
|
||||
|
||||
<!-- [Root View Only] -->
|
||||
<!-- Set margin.bottom to -20 because the next stack panel gets the 20 from the root's spacing property -->
|
||||
<StackPanel MaxWidth="{StaticResource StandardControlMaxWidth}"
|
||||
Margin="0,0,0,-20"
|
||||
Visibility="{x:Bind mtu:Converters.InvertedBooleanToVisibility(ViewModel.IsExtensionView), Mode=OneWay}">
|
||||
|
||||
<!-- Learn more about fragment extensions -->
|
||||
<HyperlinkButton x:Uid="Extensions_DisclaimerHyperlink"
|
||||
NavigateUri="https://learn.microsoft.com/en-us/windows/terminal/json-fragment-extensions" />
|
||||
|
||||
<!-- Grouping: Active Extensions -->
|
||||
<TextBlock x:Uid="Extensions_ActiveExtensionsHeader"
|
||||
Style="{StaticResource TextBlockSubHeaderStyle}" />
|
||||
<TextBlock x:Uid="Extensions_NoActiveExtensionsDisclaimer"
|
||||
Style="{StaticResource ItalicDisclaimerStyle}"
|
||||
Visibility="{x:Bind ViewModel.NoExtensionPackages, Mode=OneWay}" />
|
||||
<ItemsControl x:Name="ActiveExtensionsList"
|
||||
IsTabStop="False"
|
||||
ItemTemplateSelector="{StaticResource ExtensionPackageNavigatorTemplateSelector}"
|
||||
ItemsSource="{x:Bind ViewModel.ExtensionPackages}"
|
||||
XYFocusKeyboardNavigation="Enabled" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- [Extension View Only] -->
|
||||
<!-- Set margin.top to -20 because the previous stack panel gets the 20 from the root's spacing property -->
|
||||
<StackPanel MaxWidth="{StaticResource StandardControlMaxWidth}"
|
||||
Margin="0,-20,0,0"
|
||||
Visibility="{x:Bind ViewModel.IsExtensionView, Mode=OneWay}">
|
||||
<!-- Extension Status -->
|
||||
<muxc:Expander AutomationProperties.Name="{x:Bind ViewModel.CurrentExtensionPackage.AccessibleName, Mode=OneWay}"
|
||||
IsExpanded="True"
|
||||
Style="{StaticResource ExpanderStyle}">
|
||||
<muxc:Expander.Header>
|
||||
<Grid MinHeight="64">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!--
|
||||
BODGY
|
||||
Theoretically, you could use a ContentTemplateSelector directly. However, that doesn't work.
|
||||
For some reason, we just get the object type's ToString called and the selector gets nullptr as a parameter.
|
||||
Adding the template as a view model property is a workaround.
|
||||
-->
|
||||
<ContentPresenter Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
Content="{x:Bind ViewModel.CurrentExtensionPackage, Mode=OneWay}"
|
||||
ContentTemplate="{x:Bind ViewModel.CurrentExtensionPackageIdentifierTemplate, Mode=OneWay}" />
|
||||
|
||||
<ToggleSwitch Grid.Column="1"
|
||||
Margin="0"
|
||||
AutomationProperties.Name="{x:Bind ViewModel.CurrentExtensionPackage.AccessibleName, Mode=OneWay}"
|
||||
IsOn="{x:Bind ViewModel.CurrentExtensionPackage.Enabled, Mode=TwoWay}"
|
||||
Style="{StaticResource ToggleSwitchInExpanderStyle}"
|
||||
Tag="{x:Bind ViewModel.CurrentExtensionPackage.Package.Source, Mode=OneWay}" />
|
||||
</Grid>
|
||||
</muxc:Expander.Header>
|
||||
<muxc:Expander.Content>
|
||||
<StackPanel>
|
||||
<!-- Scope -->
|
||||
<local:SettingContainer x:Uid="Extensions_Scope"
|
||||
Content="{x:Bind ViewModel.CurrentExtensionPackage.Scope, Mode=OneWay}"
|
||||
IsTabStop="False"
|
||||
Style="{StaticResource SettingContainerWithTextContent}" />
|
||||
<!-- JSON -->
|
||||
<ItemsControl IsTabStop="False"
|
||||
ItemTemplate="{StaticResource JsonTemplate}"
|
||||
ItemsSource="{x:Bind ViewModel.CurrentExtensionPackage.FragmentExtensions, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</muxc:Expander.Content>
|
||||
</muxc:Expander>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Grouping: Modified Profiles -->
|
||||
<StackPanel>
|
||||
<TextBlock x:Uid="Extensions_ModifiedProfilesHeader"
|
||||
Style="{StaticResource TextBlockSubHeaderStyle}" />
|
||||
<TextBlock x:Uid="Extensions_NoModifiedProfilesDisclaimer"
|
||||
Style="{StaticResource ItalicDisclaimerStyle}"
|
||||
Visibility="{x:Bind ViewModel.NoProfilesModified, Mode=OneWay}" />
|
||||
<ItemsControl x:Name="ModifiedProfilesList"
|
||||
IsTabStop="False"
|
||||
ItemTemplate="{StaticResource FragmentProfileViewModelTemplate}"
|
||||
ItemsSource="{x:Bind ViewModel.ProfilesModified, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Grouping: Added Profiles -->
|
||||
<StackPanel>
|
||||
<TextBlock x:Uid="Extensions_AddedProfilesHeader"
|
||||
Style="{StaticResource TextBlockSubHeaderStyle}" />
|
||||
<TextBlock x:Uid="Extensions_NoAddedProfilesDisclaimer"
|
||||
Style="{StaticResource ItalicDisclaimerStyle}"
|
||||
Visibility="{x:Bind ViewModel.NoProfilesAdded, Mode=OneWay}" />
|
||||
<ItemsControl x:Name="AddedProfilesList"
|
||||
IsTabStop="False"
|
||||
ItemTemplate="{StaticResource FragmentProfileViewModelTemplate}"
|
||||
ItemsSource="{x:Bind ViewModel.ProfilesAdded, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Grouping: Added Color Schemes -->
|
||||
<StackPanel>
|
||||
<TextBlock x:Uid="Extensions_AddedColorSchemesHeader"
|
||||
Style="{StaticResource TextBlockSubHeaderStyle}" />
|
||||
<TextBlock x:Uid="Extensions_NoAddedColorSchemesDisclaimer"
|
||||
Style="{StaticResource ItalicDisclaimerStyle}"
|
||||
Visibility="{x:Bind ViewModel.NoSchemesAdded, Mode=OneWay}" />
|
||||
<ItemsControl x:Name="AddedColorSchemesList"
|
||||
IsTabStop="False"
|
||||
ItemTemplate="{StaticResource FragmentColorSchemeViewModelTemplate}"
|
||||
ItemsSource="{x:Bind ViewModel.ColorSchemesAdded, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Page>
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "Compatibility.h"
|
||||
#include "Rendering.h"
|
||||
#include "RenderingViewModel.h"
|
||||
#include "Extensions.h"
|
||||
#include "Actions.h"
|
||||
#include "ProfileViewModel.h"
|
||||
#include "GlobalAppearance.h"
|
||||
@@ -44,6 +45,7 @@ static const std::wstring_view renderingTag{ L"Rendering_Nav" };
|
||||
static const std::wstring_view compatibilityTag{ L"Compatibility_Nav" };
|
||||
static const std::wstring_view actionsTag{ L"Actions_Nav" };
|
||||
static const std::wstring_view newTabMenuTag{ L"NewTabMenu_Nav" };
|
||||
static const std::wstring_view extensionsTag{ L"Extensions_Nav" };
|
||||
static const std::wstring_view globalProfileTag{ L"GlobalProfile_Nav" };
|
||||
static const std::wstring_view addProfileTag{ L"AddProfile" };
|
||||
static const std::wstring_view colorSchemesTag{ L"ColorSchemes_Nav" };
|
||||
@@ -111,6 +113,33 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
|
||||
}
|
||||
});
|
||||
|
||||
auto extensionsVMImpl = winrt::make_self<ExtensionsViewModel>(_settingsClone, _colorSchemesPageVM);
|
||||
extensionsVMImpl->NavigateToProfileRequested({ this, &MainPage::_NavigateToProfileHandler });
|
||||
extensionsVMImpl->NavigateToColorSchemeRequested({ this, &MainPage::_NavigateToColorSchemeHandler });
|
||||
_extensionsVM = *extensionsVMImpl;
|
||||
_extensionsViewModelChangedRevoker = _extensionsVM.PropertyChanged(winrt::auto_revoke, [=](auto&&, const PropertyChangedEventArgs& args) {
|
||||
const auto settingName{ args.PropertyName() };
|
||||
if (settingName == L"CurrentExtensionPackage")
|
||||
{
|
||||
if (const auto& currentExtensionPackage = _extensionsVM.CurrentExtensionPackage())
|
||||
{
|
||||
const auto& pkg = currentExtensionPackage.Package();
|
||||
const auto label = pkg.DisplayName().empty() ? pkg.Source() : pkg.DisplayName();
|
||||
const auto crumb = winrt::make<Breadcrumb>(box_value(currentExtensionPackage), label, BreadcrumbSubPage::Extensions_Extension);
|
||||
_breadcrumbs.Append(crumb);
|
||||
SettingsMainPage_ScrollViewer().ScrollToVerticalOffset(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If we don't have a current extension package, we're at the root of the Extensions page
|
||||
_breadcrumbs.Clear();
|
||||
const auto crumb = winrt::make<Breadcrumb>(box_value(extensionsTag), RS_(L"Nav_Extensions/Content"), BreadcrumbSubPage::None);
|
||||
_breadcrumbs.Append(crumb);
|
||||
}
|
||||
contentFrame().Navigate(xaml_typename<Editor::Extensions>(), _extensionsVM);
|
||||
}
|
||||
});
|
||||
|
||||
// Make sure to initialize the profiles _after_ we have initialized the color schemes page VM, because we pass
|
||||
// that VM into the appearance VMs within the profiles
|
||||
_InitializeProfilesList();
|
||||
@@ -161,6 +190,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
|
||||
// Update the Nav State with the new version of the settings
|
||||
_colorSchemesPageVM.UpdateSettings(_settingsClone);
|
||||
_newTabMenuPageVM.UpdateSettings(_settingsClone);
|
||||
_extensionsVM.UpdateSettings(_settingsClone, _colorSchemesPageVM);
|
||||
|
||||
// We'll update the profile in the _profilesNavState whenever we actually navigate to one
|
||||
|
||||
@@ -182,7 +212,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
|
||||
{
|
||||
// found the one that was selected before the refresh
|
||||
SettingsNav().SelectedItem(item);
|
||||
_Navigate(*stringTag, crumb->SubPage());
|
||||
_Navigate(*breadcrumbStringTag, crumb->SubPage());
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -197,6 +227,17 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (const auto& breadcrumbExtensionPackage{ crumb->Tag().try_as<Editor::ExtensionPackageViewModel>() })
|
||||
{
|
||||
if (stringTag == extensionsTag)
|
||||
{
|
||||
// navigate to the NewTabMenu page,
|
||||
// _Navigate() will handle trying to find the right subpage
|
||||
SettingsNav().SelectedItem(item);
|
||||
_Navigate(breadcrumbExtensionPackage, BreadcrumbSubPage::NewTabMenu_Folder);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (const auto& profileTag{ tag.try_as<ProfileViewModel>() })
|
||||
{
|
||||
@@ -445,6 +486,21 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
|
||||
_breadcrumbs.Append(crumb);
|
||||
}
|
||||
}
|
||||
else if (clickedItemTag == extensionsTag)
|
||||
{
|
||||
if (_extensionsVM.CurrentExtensionPackage())
|
||||
{
|
||||
// Setting CurrentExtensionPackage triggers the PropertyChanged event,
|
||||
// which will navigate to the correct page and update the breadcrumbs appropriately
|
||||
_extensionsVM.CurrentExtensionPackage(nullptr);
|
||||
}
|
||||
else
|
||||
{
|
||||
contentFrame().Navigate(xaml_typename<Editor::Extensions>(), _extensionsVM);
|
||||
const auto crumb = winrt::make<Breadcrumb>(box_value(clickedItemTag), RS_(L"Nav_Extensions/Content"), BreadcrumbSubPage::None);
|
||||
_breadcrumbs.Append(crumb);
|
||||
}
|
||||
}
|
||||
else if (clickedItemTag == globalProfileTag)
|
||||
{
|
||||
auto profileVM{ _viewModelForProfile(_settingsClone.ProfileDefaults(), _settingsClone, Dispatcher()) };
|
||||
@@ -575,6 +631,40 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
|
||||
}
|
||||
}
|
||||
|
||||
void MainPage::_Navigate(const Editor::ExtensionPackageViewModel& extPkgVM, BreadcrumbSubPage subPage)
|
||||
{
|
||||
_PreNavigateHelper();
|
||||
|
||||
contentFrame().Navigate(xaml_typename<Editor::Extensions>(), _extensionsVM);
|
||||
const auto crumb = winrt::make<Breadcrumb>(box_value(extensionsTag), RS_(L"Nav_Extensions/Content"), BreadcrumbSubPage::None);
|
||||
_breadcrumbs.Append(crumb);
|
||||
|
||||
if (subPage == BreadcrumbSubPage::None)
|
||||
{
|
||||
_extensionsVM.CurrentExtensionPackage(nullptr);
|
||||
}
|
||||
else
|
||||
{
|
||||
bool found = false;
|
||||
for (const auto& pkgVM : _extensionsVM.ExtensionPackages())
|
||||
{
|
||||
if (pkgVM.Package().Source() == extPkgVM.Package().Source())
|
||||
{
|
||||
// Take advantage of the PropertyChanged event to navigate
|
||||
// to the correct extension package and build the breadcrumbs as we go
|
||||
_extensionsVM.CurrentExtensionPackage(pkgVM);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found)
|
||||
{
|
||||
// If we couldn't find a reasonable match, just go back to the root
|
||||
_extensionsVM.CurrentExtensionPackage(nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MainPage::OpenJsonTapped(const IInspectable& /*sender*/, const Windows::UI::Xaml::Input::TappedRoutedEventArgs& /*args*/)
|
||||
{
|
||||
const auto window = CoreWindow::GetForCurrentThread();
|
||||
@@ -621,6 +711,10 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
|
||||
{
|
||||
_Navigate(*ntmEntryViewModel, subPage);
|
||||
}
|
||||
else if (const auto extPkgViewModel = tag.try_as<ExtensionPackageViewModel>())
|
||||
{
|
||||
_Navigate(*extPkgViewModel, subPage);
|
||||
}
|
||||
else
|
||||
{
|
||||
_Navigate(tag.as<hstring>(), subPage);
|
||||
@@ -818,6 +912,35 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
|
||||
return _breadcrumbs;
|
||||
}
|
||||
|
||||
void MainPage::_NavigateToProfileHandler(const IInspectable& /*sender*/, winrt::guid profileGuid)
|
||||
{
|
||||
for (auto&& menuItem : _menuItemSource)
|
||||
{
|
||||
if (const auto& navViewItem{ menuItem.try_as<MUX::Controls::NavigationViewItem>() })
|
||||
{
|
||||
if (const auto& tag{ navViewItem.Tag() })
|
||||
{
|
||||
if (const auto& profileTag{ tag.try_as<ProfileViewModel>() })
|
||||
{
|
||||
if (profileTag->OriginalProfileGuid() == profileGuid)
|
||||
{
|
||||
SettingsNav().SelectedItem(menuItem);
|
||||
_Navigate(*profileTag, BreadcrumbSubPage::None);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Silently fail if the profile wasn't found
|
||||
}
|
||||
|
||||
void MainPage::_NavigateToColorSchemeHandler(const IInspectable& /*sender*/, const IInspectable& /*args*/)
|
||||
{
|
||||
SettingsNav().SelectedItem(ColorSchemesNavItem());
|
||||
_Navigate(hstring{ colorSchemesTag }, BreadcrumbSubPage::ColorSchemes_Edit);
|
||||
}
|
||||
|
||||
winrt::Windows::UI::Xaml::Media::Brush MainPage::BackgroundBrush()
|
||||
{
|
||||
return SettingsNav().Background();
|
||||
|
||||
@@ -45,6 +45,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
|
||||
winrt::Windows::UI::Xaml::Media::Brush BackgroundBrush();
|
||||
|
||||
Windows::Foundation::Collections::IObservableVector<IInspectable> Breadcrumbs() noexcept;
|
||||
Editor::ExtensionsViewModel ExtensionsVM() const noexcept { return _extensionsVM; }
|
||||
|
||||
til::typed_event<Windows::Foundation::IInspectable, Model::SettingsTarget> OpenJson;
|
||||
|
||||
@@ -70,16 +71,21 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
|
||||
void _Navigate(hstring clickedItemTag, BreadcrumbSubPage subPage);
|
||||
void _Navigate(const Editor::ProfileViewModel& profile, BreadcrumbSubPage subPage);
|
||||
void _Navigate(const Editor::NewTabMenuEntryViewModel& ntmEntryVM, BreadcrumbSubPage subPage);
|
||||
void _Navigate(const Editor::ExtensionPackageViewModel& extPkgVM, BreadcrumbSubPage subPage);
|
||||
void _NavigateToProfileHandler(const IInspectable& sender, winrt::guid profileGuid);
|
||||
void _NavigateToColorSchemeHandler(const IInspectable& sender, const IInspectable& args);
|
||||
|
||||
void _UpdateBackgroundForMica();
|
||||
void _MoveXamlParsedNavItemsIntoItemSource();
|
||||
|
||||
winrt::Microsoft::Terminal::Settings::Editor::ColorSchemesPageViewModel _colorSchemesPageVM{ nullptr };
|
||||
winrt::Microsoft::Terminal::Settings::Editor::NewTabMenuViewModel _newTabMenuPageVM{ nullptr };
|
||||
winrt::Microsoft::Terminal::Settings::Editor::ExtensionsViewModel _extensionsVM{ nullptr };
|
||||
|
||||
Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker _profileViewModelChangedRevoker;
|
||||
Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker _colorSchemesPageViewModelChangedRevoker;
|
||||
Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker _ntmViewModelChangedRevoker;
|
||||
Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker _extensionsViewModelChangedRevoker;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import "Extensions.idl";
|
||||
|
||||
namespace Microsoft.Terminal.Settings.Editor
|
||||
{
|
||||
// Due to a XAML Compiler bug, it is hard for us to propagate an HWND into a XAML-using runtimeclass.
|
||||
@@ -20,7 +22,8 @@ namespace Microsoft.Terminal.Settings.Editor
|
||||
Profile_Terminal,
|
||||
Profile_Advanced,
|
||||
ColorSchemes_Edit,
|
||||
NewTabMenu_Folder
|
||||
NewTabMenu_Folder,
|
||||
Extensions_Extension
|
||||
};
|
||||
|
||||
runtimeclass Breadcrumb : Windows.Foundation.IStringable
|
||||
@@ -42,6 +45,7 @@ namespace Microsoft.Terminal.Settings.Editor
|
||||
void SetHostingWindow(UInt64 window);
|
||||
|
||||
Windows.Foundation.Collections.IObservableVector<IInspectable> Breadcrumbs { get; };
|
||||
ExtensionsViewModel ExtensionsVM { get; };
|
||||
|
||||
Windows.UI.Xaml.Media.Brush BackgroundBrush { get; };
|
||||
}
|
||||
|
||||
@@ -120,7 +120,8 @@
|
||||
</muxc:NavigationViewItem.Icon>
|
||||
</muxc:NavigationViewItem>
|
||||
|
||||
<muxc:NavigationViewItem x:Uid="Nav_ColorSchemes"
|
||||
<muxc:NavigationViewItem x:Name="ColorSchemesNavItem"
|
||||
x:Uid="Nav_ColorSchemes"
|
||||
Tag="ColorSchemes_Nav">
|
||||
<muxc:NavigationViewItem.Icon>
|
||||
<FontIcon Glyph="" />
|
||||
@@ -155,6 +156,17 @@
|
||||
</muxc:NavigationViewItem.Icon>
|
||||
</muxc:NavigationViewItem>
|
||||
|
||||
<muxc:NavigationViewItem x:Uid="Nav_Extensions"
|
||||
Tag="Extensions_Nav">
|
||||
<muxc:NavigationViewItem.Icon>
|
||||
<FontIcon Glyph="" />
|
||||
</muxc:NavigationViewItem.Icon>
|
||||
<muxc:NavigationViewItem.InfoBadge>
|
||||
<muxc:InfoBadge Visibility="{x:Bind ExtensionsVM.DisplayBadge, Mode=OneWay}"
|
||||
Value="1" />
|
||||
</muxc:NavigationViewItem.InfoBadge>
|
||||
</muxc:NavigationViewItem>
|
||||
|
||||
<muxc:NavigationViewItemHeader x:Uid="Nav_Profiles" />
|
||||
|
||||
<muxc:NavigationViewItem x:Name="BaseLayerMenuItem"
|
||||
|
||||
@@ -125,6 +125,10 @@
|
||||
<DependentUpon>NewTabMenuViewModel.idl</DependentUpon>
|
||||
<SubType>Code</SubType>
|
||||
</ClInclude>
|
||||
<ClInclude Include="Extensions.h">
|
||||
<DependentUpon>Extensions.xaml</DependentUpon>
|
||||
<SubType>Code</SubType>
|
||||
</ClInclude>
|
||||
<ClInclude Include="Profiles_Base.h">
|
||||
<DependentUpon>Profiles_Base.xaml</DependentUpon>
|
||||
<SubType>Code</SubType>
|
||||
@@ -199,6 +203,9 @@
|
||||
<Page Include="MainPage.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
</Page>
|
||||
<Page Include="Extensions.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
</Page>
|
||||
<Page Include="Profiles_Base.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
</Page>
|
||||
@@ -309,6 +316,10 @@
|
||||
<DependentUpon>NewTabMenuViewModel.idl</DependentUpon>
|
||||
<SubType>Code</SubType>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Extensions.cpp">
|
||||
<DependentUpon>Extensions.xaml</DependentUpon>
|
||||
<SubType>Code</SubType>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Profiles_Base.cpp">
|
||||
<DependentUpon>Profiles_Base.xaml</DependentUpon>
|
||||
<SubType>Code</SubType>
|
||||
@@ -408,6 +419,10 @@
|
||||
<Midl Include="GlobalAppearanceViewModel.idl" />
|
||||
<Midl Include="LaunchViewModel.idl" />
|
||||
<Midl Include="NewTabMenuViewModel.idl" />
|
||||
<Midl Include="Extensions.idl">
|
||||
<DependentUpon>Extensions.xaml</DependentUpon>
|
||||
<SubType>Code</SubType>
|
||||
</Midl>
|
||||
<Midl Include="Profiles_Base.idl">
|
||||
<DependentUpon>Profiles_Base.xaml</DependentUpon>
|
||||
<SubType>Code</SubType>
|
||||
|
||||
@@ -22,8 +22,6 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_entryTemplateSelector = Resources().Lookup(box_value(L"NewTabMenuEntryTemplateSelector")).as<Editor::NewTabMenuEntryTemplateSelector>();
|
||||
|
||||
// Ideally, we'd bind IsEnabled to something like mtu:Converters.isEmpty(NewTabMenuListView.SelectedItems.Size) in the XAML,
|
||||
// but the XAML compiler can't find NewTabMenuListView when we try that. Rather than copying the list of selected items over
|
||||
// to the view model, we'll just do this instead (much simpler).
|
||||
|
||||
@@ -42,7 +42,6 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
|
||||
WINRT_OBSERVABLE_PROPERTY(Editor::NewTabMenuViewModel, ViewModel, _PropertyChangedHandlers, nullptr);
|
||||
|
||||
private:
|
||||
Editor::NewTabMenuEntryTemplateSelector _entryTemplateSelector{ nullptr };
|
||||
Editor::NewTabMenuEntryViewModel _draggedEntry{ nullptr };
|
||||
|
||||
void _ScrollToEntry(const Editor::NewTabMenuEntryViewModel& entry);
|
||||
|
||||
@@ -321,7 +321,7 @@
|
||||
<TextBlock x:Uid="NewTabMenu_CurrentFolderTextBlock"
|
||||
Style="{StaticResource TextBlockSubHeaderStyle}" />
|
||||
|
||||
<!-- TODO CARLOS: Icon -->
|
||||
<!-- TODO GH #18281: Icon -->
|
||||
<!-- Once PR #17965 merges, we can add that kind of control to set an icon -->
|
||||
|
||||
<!-- Name -->
|
||||
|
||||
@@ -684,6 +684,10 @@
|
||||
<value>Actions</value>
|
||||
<comment>Header for the "actions" menu item. This navigates to a page that lets you see and modify commands, key bindings, and actions that can be done in the app.</comment>
|
||||
</data>
|
||||
<data name="Nav_Extensions.Content" xml:space="preserve">
|
||||
<value>Extensions</value>
|
||||
<comment>Header for the "extensions" menu item. This navigates to a page that lets you see and modify extensions for the app.</comment>
|
||||
</data>
|
||||
<data name="Profile_OpacitySlider.[using:Windows.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Background opacity</value>
|
||||
<comment>Name for a control to determine the level of opacity for the background of the control. The user can choose to make the background of the app more or less opaque.</comment>
|
||||
@@ -2344,4 +2348,63 @@
|
||||
<value>This option is managed by enterprise policy and cannot be changed here.</value>
|
||||
<comment>This is displayed in concordance with Globals_StartOnUserLogin if the enterprise administrator has taken control of this setting.</comment>
|
||||
</data>
|
||||
<data name="Extensions_ActiveExtensionsHeader.Text" xml:space="preserve">
|
||||
<value>Active Extensions</value>
|
||||
</data>
|
||||
<data name="Extensions_ModifiedProfilesHeader.Text" xml:space="preserve">
|
||||
<value>Modified Profiles</value>
|
||||
</data>
|
||||
<data name="Extensions_AddedProfilesHeader.Text" xml:space="preserve">
|
||||
<value>Added Profiles</value>
|
||||
</data>
|
||||
<data name="Extensions_AddedColorSchemesHeader.Text" xml:space="preserve">
|
||||
<value>Added Color Schemes</value>
|
||||
</data>
|
||||
<data name="Extensions_NoActiveExtensionsDisclaimer.Text" xml:space="preserve">
|
||||
<value>None</value>
|
||||
<comment>Text displayed when no extensions are available. Shown in place of a list of entries.</comment>
|
||||
</data>
|
||||
<data name="Extensions_NoModifiedProfilesDisclaimer.Text" xml:space="preserve">
|
||||
<value>None</value>
|
||||
<comment>Text displayed when no profiles were modified. Shown in place of a list of entries.</comment>
|
||||
</data>
|
||||
<data name="Extensions_NoAddedColorSchemesDisclaimer.Text" xml:space="preserve">
|
||||
<value>None</value>
|
||||
<comment>Text displayed when no color schemes were added. Shown in place of a list of entries.</comment>
|
||||
</data>
|
||||
<data name="Extensions_NoAddedProfilesDisclaimer.Text" xml:space="preserve">
|
||||
<value>None</value>
|
||||
<comment>Text displayed when no profiles were added. Shown in place of a list of entries.</comment>
|
||||
</data>
|
||||
<data name="Extensions_DisclaimerHyperlink.Content" xml:space="preserve">
|
||||
<value>Learn more about fragment extensions</value>
|
||||
</data>
|
||||
<data name="Extensions_NavigateToProfileButton.[using:Windows.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Navigate to profile</value>
|
||||
</data>
|
||||
<data name="Extensions_NavigateToProfileButton.[using:Windows.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Navigate to profile</value>
|
||||
</data>
|
||||
<data name="Extensions_NavigateToColorSchemeButton.[using:Windows.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Navigate to color scheme</value>
|
||||
</data>
|
||||
<data name="Extensions_NavigateToColorSchemeButton.[using:Windows.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Navigate to color scheme</value>
|
||||
</data>
|
||||
<data name="Extensions_ScopeUser" xml:space="preserve">
|
||||
<value>Current User</value>
|
||||
<comment>Label for the installation scope of an extension.</comment>
|
||||
</data>
|
||||
<data name="Extensions_ScopeSystem" xml:space="preserve">
|
||||
<value>All Users</value>
|
||||
<comment>Label for the installation scope of an extension</comment>
|
||||
</data>
|
||||
<data name="Extensions_Scope.Header" xml:space="preserve">
|
||||
<value>Scope</value>
|
||||
<comment>Header for the installation scope of the extension</comment>
|
||||
</data>
|
||||
<data name="Extension_StateDisabled.Text" xml:space="preserve">
|
||||
<value>Disabled</value>
|
||||
<comment>Text displayed when an extension is disabled</comment>
|
||||
</data>
|
||||
</root>
|
||||
@@ -179,13 +179,18 @@
|
||||
<Setter Property="FontFamily" Value="Segoe UI, Segoe Fluent Icons, Segoe MDL2 Assets" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="SettingContainerCurrentValueTextBlockStyle"
|
||||
BasedOn="{StaticResource SettingsPageItemDescriptionStyle}"
|
||||
TargetType="TextBlock">
|
||||
<Setter Property="MaxWidth" Value="250" />
|
||||
<Setter Property="Margin" Value="0,0,-16,0" />
|
||||
<Setter Property="HorizontalAlignment" Value="Right" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="FontFamily" Value="Segoe UI, Segoe Fluent Icons, Segoe MDL2 Assets" />
|
||||
</Style>
|
||||
|
||||
<DataTemplate x:Key="ExpanderSettingContainerStringPreviewTemplate">
|
||||
<TextBlock MaxWidth="250"
|
||||
Margin="0,0,-16,0"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="Segoe UI, Segoe Fluent Icons, Segoe MDL2 Assets"
|
||||
Style="{StaticResource SettingsPageItemDescriptionStyle}"
|
||||
<TextBlock Style="{StaticResource SettingContainerCurrentValueTextBlockStyle}"
|
||||
Text="{Binding}" />
|
||||
</DataTemplate>
|
||||
|
||||
@@ -228,6 +233,45 @@
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- A basic setting container displaying immutable text as content -->
|
||||
<Style x:Key="SettingContainerWithTextContent"
|
||||
TargetType="local:SettingContainer">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="local:SettingContainer">
|
||||
<Grid AutomationProperties.Name="{TemplateBinding Header}"
|
||||
Style="{StaticResource NonExpanderGrid}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Grid.Column="0"
|
||||
Style="{StaticResource StackPanelInExpanderStyle}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Style="{StaticResource SettingsPageItemHeaderStyle}"
|
||||
Text="{TemplateBinding Header}" />
|
||||
<Button x:Name="ResetButton"
|
||||
Style="{StaticResource SettingContainerResetButtonStyle}">
|
||||
<FontIcon Glyph=""
|
||||
Style="{StaticResource SettingContainerFontIconStyle}" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
<TextBlock x:Name="HelpTextBlock"
|
||||
Style="{StaticResource SettingsPageItemDescriptionStyle}"
|
||||
Text="{TemplateBinding HelpText}" />
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Column="1"
|
||||
Margin="0,0,8,0"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Style="{ThemeResource SecondaryTextBlockStyle}"
|
||||
Text="{TemplateBinding Content}" />
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!--
|
||||
A setting container for a setting that has no additional options.
|
||||
Includes space for an icon on the left side of the header.
|
||||
@@ -302,8 +346,8 @@
|
||||
<StackPanel Grid.Column="0"
|
||||
Style="{StaticResource StackPanelInExpanderStyle}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Style="{StaticResource SettingsPageItemHeaderStyle}"
|
||||
Text="{TemplateBinding Header}" />
|
||||
<ContentPresenter Content="{TemplateBinding Header}"
|
||||
Style="{StaticResource SettingsPageItemHeaderStyle}" />
|
||||
<Button x:Name="ResetButton"
|
||||
Style="{StaticResource SettingContainerResetButtonStyle}">
|
||||
<FontIcon Glyph=""
|
||||
|
||||
@@ -309,6 +309,35 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
|
||||
_throttler();
|
||||
}
|
||||
|
||||
bool ApplicationState::DismissBadge(const hstring& badgeId)
|
||||
{
|
||||
bool inserted{ false };
|
||||
{
|
||||
const auto state = _state.lock();
|
||||
if (!state->DismissedBadges)
|
||||
{
|
||||
state->DismissedBadges = std::unordered_set<hstring>{};
|
||||
}
|
||||
if (!state->DismissedBadges->contains(badgeId))
|
||||
{
|
||||
state->DismissedBadges->insert(badgeId);
|
||||
inserted = true;
|
||||
}
|
||||
}
|
||||
_throttler();
|
||||
return inserted;
|
||||
}
|
||||
|
||||
bool ApplicationState::BadgeDismissed(const hstring& badgeId) const
|
||||
{
|
||||
const auto state = _state.lock_shared();
|
||||
if (state->DismissedBadges)
|
||||
{
|
||||
return state->DismissedBadges->contains(badgeId);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Generate all getter/setters
|
||||
#define MTSM_APPLICATION_STATE_GEN(source, type, name, key, ...) \
|
||||
type ApplicationState::name() const noexcept \
|
||||
|
||||
@@ -40,7 +40,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
|
||||
X(FileSource::Local, Windows::Foundation::Collections::IVector<Model::WindowLayout>, PersistedWindowLayouts, "persistedWindowLayouts") \
|
||||
X(FileSource::Shared, Windows::Foundation::Collections::IVector<hstring>, RecentCommands, "recentCommands") \
|
||||
X(FileSource::Shared, Windows::Foundation::Collections::IVector<winrt::Microsoft::Terminal::Settings::Model::InfoBarMessage>, DismissedMessages, "dismissedMessages") \
|
||||
X(FileSource::Local, Windows::Foundation::Collections::IVector<hstring>, AllowedCommandlines, "allowedCommandlines")
|
||||
X(FileSource::Local, Windows::Foundation::Collections::IVector<hstring>, AllowedCommandlines, "allowedCommandlines") \
|
||||
X(FileSource::Local, std::unordered_set<hstring>, DismissedBadges, "dismissedBadges")
|
||||
|
||||
struct WindowLayout : WindowLayoutT<WindowLayout>
|
||||
{
|
||||
@@ -70,6 +71,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
|
||||
Json::Value ToJson(FileSource parseSource) const noexcept;
|
||||
|
||||
void AppendPersistedWindowLayout(Model::WindowLayout layout);
|
||||
bool DismissBadge(const hstring& badgeId);
|
||||
bool BadgeDismissed(const hstring& badgeId) const;
|
||||
|
||||
// State getters/setters
|
||||
#define MTSM_APPLICATION_STATE_GEN(source, type, name, key, ...) \
|
||||
|
||||
@@ -33,6 +33,8 @@ namespace Microsoft.Terminal.Settings.Model
|
||||
void Reset();
|
||||
|
||||
void AppendPersistedWindowLayout(WindowLayout layout);
|
||||
Boolean DismissBadge(String badgeId);
|
||||
Boolean BadgeDismissed(String badgeId);
|
||||
|
||||
String SettingsHash;
|
||||
Windows.Foundation.Collections.IVector<WindowLayout> PersistedWindowLayouts;
|
||||
|
||||
@@ -8,16 +8,29 @@
|
||||
|
||||
#include "../../inc/DefaultSettings.h"
|
||||
#include "DynamicProfileUtils.h"
|
||||
#include <LibraryResources.h>
|
||||
|
||||
using namespace ::Microsoft::Terminal::Settings::Model;
|
||||
using namespace winrt::Microsoft::Terminal::Settings::Model;
|
||||
using namespace winrt::Microsoft::Terminal::TerminalConnection;
|
||||
|
||||
std::wstring_view GENERATOR_ICON_PATH{ L"ms-appx:///ProfileGeneratorIcons/AzureCloudShell.png" };
|
||||
|
||||
std::wstring_view AzureCloudShellGenerator::GetNamespace() const noexcept
|
||||
{
|
||||
return AzureGeneratorNamespace;
|
||||
}
|
||||
|
||||
std::wstring_view AzureCloudShellGenerator::GetDisplayName() const noexcept
|
||||
{
|
||||
return RS_(L"AzureCloudShellGeneratorDisplayName");
|
||||
}
|
||||
|
||||
std::wstring_view AzureCloudShellGenerator::GetIcon() const noexcept
|
||||
{
|
||||
return GENERATOR_ICON_PATH;
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Checks if the Azure Cloud shell is available on this platform, and if it
|
||||
// is, creates a profile to be able to launch it.
|
||||
@@ -25,7 +38,7 @@ std::wstring_view AzureCloudShellGenerator::GetNamespace() 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())
|
||||
{
|
||||
|
||||
@@ -25,6 +25,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model
|
||||
{
|
||||
public:
|
||||
std::wstring_view GetNamespace() const noexcept override;
|
||||
void GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles) const 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) override;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -113,6 +113,17 @@ Model::CascadiaSettings CascadiaSettings::Copy() const
|
||||
settings->_globals = _globals->Copy();
|
||||
settings->_allProfiles = winrt::single_threaded_observable_vector(std::move(allProfiles));
|
||||
settings->_activeProfiles = winrt::single_threaded_observable_vector(std::move(activeProfiles));
|
||||
|
||||
// copy extension packages
|
||||
{
|
||||
std::vector<Model::ExtensionPackage> extensions;
|
||||
extensions.reserve(_extensionPackages.Size());
|
||||
for (const auto& extension : _extensionPackages)
|
||||
{
|
||||
extensions.emplace_back(get_self<ExtensionPackage>(extension)->Copy());
|
||||
}
|
||||
settings->_extensionPackages = winrt::single_threaded_vector(std::move(extensions));
|
||||
}
|
||||
}
|
||||
|
||||
// load errors
|
||||
@@ -131,6 +142,51 @@ Model::CascadiaSettings CascadiaSettings::Copy() const
|
||||
return *settings;
|
||||
}
|
||||
|
||||
Model::FragmentSettings FragmentSettings::Copy() const
|
||||
{
|
||||
auto fragment{ winrt::make_self<FragmentSettings>(_source, _Json, _jsonSource) };
|
||||
|
||||
std::vector<Model::FragmentProfileEntry> modifiedProfiles;
|
||||
modifiedProfiles.reserve(_modifiedProfiles.Size());
|
||||
for (const auto& entry : _modifiedProfiles)
|
||||
{
|
||||
modifiedProfiles.emplace_back(winrt::make<FragmentProfileEntry>(entry.ProfileGuid(), entry.Json()));
|
||||
}
|
||||
fragment->_modifiedProfiles = winrt::single_threaded_observable_vector(std::move(modifiedProfiles));
|
||||
|
||||
std::vector<Model::FragmentProfileEntry> newProfiles;
|
||||
newProfiles.reserve(_newProfiles.Size());
|
||||
for (const auto& entry : _newProfiles)
|
||||
{
|
||||
newProfiles.emplace_back(winrt::make<FragmentProfileEntry>(entry.ProfileGuid(), entry.Json()));
|
||||
}
|
||||
fragment->_newProfiles = winrt::single_threaded_observable_vector(std::move(newProfiles));
|
||||
|
||||
std::vector<Model::FragmentColorSchemeEntry> colorSchemes;
|
||||
colorSchemes.reserve(_colorSchemes.Size());
|
||||
for (const auto& entry : _colorSchemes)
|
||||
{
|
||||
colorSchemes.emplace_back(winrt::make<FragmentColorSchemeEntry>(entry.ColorSchemeName(), entry.Json()));
|
||||
}
|
||||
fragment->_colorSchemes = winrt::single_threaded_observable_vector(std::move(colorSchemes));
|
||||
|
||||
return *fragment;
|
||||
}
|
||||
|
||||
Model::ExtensionPackage ExtensionPackage::Copy() const
|
||||
{
|
||||
auto extPkg{ winrt::make_self<ExtensionPackage>(_source, _scope) };
|
||||
extPkg->Icon(_Icon);
|
||||
extPkg->DisplayName(_DisplayName);
|
||||
|
||||
for (const auto& frag : _fragments)
|
||||
{
|
||||
extPkg->Fragments().Append(get_self<FragmentSettings>(frag)->Copy());
|
||||
}
|
||||
|
||||
return *extPkg;
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Finds a profile that matches the given GUID. If there is no profile in this
|
||||
// settings object that matches, returns nullptr.
|
||||
@@ -173,6 +229,11 @@ IObservableVector<Model::Profile> CascadiaSettings::ActiveProfiles() const noexc
|
||||
return _activeProfiles;
|
||||
}
|
||||
|
||||
IVectorView<Model::ExtensionPackage> CascadiaSettings::Extensions() const noexcept
|
||||
{
|
||||
return _extensionPackages.GetView();
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Returns the globally configured keybindings
|
||||
// Arguments:
|
||||
|
||||
@@ -18,6 +18,10 @@ Author(s):
|
||||
#pragma once
|
||||
|
||||
#include "CascadiaSettings.g.h"
|
||||
#include "FragmentSettings.g.h"
|
||||
#include "FragmentProfileEntry.g.h"
|
||||
#include "FragmentColorSchemeEntry.g.h"
|
||||
#include "ExtensionPackage.g.h"
|
||||
|
||||
#include "GlobalAppSettings.h"
|
||||
#include "Profile.h"
|
||||
@@ -39,6 +43,30 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
|
||||
std::runtime_error(message) {}
|
||||
};
|
||||
|
||||
struct ExtensionPackage : ExtensionPackageT<ExtensionPackage>
|
||||
{
|
||||
public:
|
||||
ExtensionPackage(hstring source, FragmentScope scope) :
|
||||
_source{ source },
|
||||
_scope{ scope },
|
||||
_fragments{ winrt::single_threaded_vector<Model::FragmentSettings>() } {}
|
||||
|
||||
Model::ExtensionPackage Copy() const;
|
||||
|
||||
hstring Source() const noexcept { return _source; }
|
||||
FragmentScope Scope() const noexcept { return _scope; }
|
||||
Windows::Foundation::Collections::IVectorView<Model::FragmentSettings> FragmentsView() const noexcept { return _fragments.GetView(); }
|
||||
Windows::Foundation::Collections::IVector<Model::FragmentSettings> Fragments() const noexcept { return _fragments; }
|
||||
|
||||
WINRT_PROPERTY(hstring, Icon);
|
||||
WINRT_PROPERTY(hstring, DisplayName);
|
||||
|
||||
private:
|
||||
hstring _source;
|
||||
FragmentScope _scope;
|
||||
Windows::Foundation::Collections::IVector<Model::FragmentSettings> _fragments;
|
||||
};
|
||||
|
||||
struct ParsedSettings
|
||||
{
|
||||
winrt::com_ptr<implementation::GlobalAppSettings> globals;
|
||||
@@ -70,6 +98,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
|
||||
|
||||
ParsedSettings inboxSettings;
|
||||
ParsedSettings userSettings;
|
||||
std::unordered_map<hstring, winrt::com_ptr<implementation::ExtensionPackage>> extensionPackageMap;
|
||||
bool duplicateProfile = false;
|
||||
|
||||
private:
|
||||
@@ -88,13 +117,15 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
|
||||
static const Json::Value& _getJSONValue(const Json::Value& json, const std::string_view& key) noexcept;
|
||||
std::span<const winrt::com_ptr<implementation::Profile>> _getNonUserOriginProfiles() const;
|
||||
void _parse(const OriginTag origin, const winrt::hstring& source, const std::string_view& content, ParsedSettings& settings);
|
||||
void _parseFragment(const winrt::hstring& source, const std::string_view& content, ParsedSettings& settings);
|
||||
void _parseFragment(const winrt::hstring& source, const std::string_view& content, ParsedSettings& settings, FragmentScope scope, std::wstring_view jsonFilename, bool applyToSettings);
|
||||
static JsonSettings _parseJson(const std::string_view& content);
|
||||
static winrt::com_ptr<implementation::Profile> _parseProfile(const OriginTag origin, const winrt::hstring& source, const Json::Value& profileJson);
|
||||
void _appendProfile(winrt::com_ptr<Profile>&& profile, const winrt::guid& guid, ParsedSettings& settings);
|
||||
void _addUserProfileParent(const winrt::com_ptr<implementation::Profile>& profile);
|
||||
void _addOrMergeUserColorScheme(const winrt::com_ptr<implementation::ColorScheme>& colorScheme);
|
||||
void _executeGenerator(const IDynamicProfileGenerator& generator);
|
||||
bool _addOrMergeUserColorScheme(const winrt::com_ptr<implementation::ColorScheme>& colorScheme);
|
||||
void _executeGenerator(IDynamicProfileGenerator& generator);
|
||||
void _cleanupPowerShellInstaller(bool isPowerShellInstalled);
|
||||
winrt::com_ptr<implementation::ExtensionPackage> _registerFragment(const winrt::Microsoft::Terminal::Settings::Model::FragmentSettings& fragment, FragmentScope scope);
|
||||
|
||||
std::unordered_set<winrt::hstring, til::transparent_hstring_hash, til::transparent_hstring_equal_to> _ignoredNamespaces;
|
||||
std::set<std::string> themesChangeLog;
|
||||
@@ -127,6 +158,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
|
||||
winrt::Windows::Foundation::Collections::IObservableVector<Model::Profile> AllProfiles() const noexcept;
|
||||
winrt::Windows::Foundation::Collections::IObservableVector<Model::Profile> ActiveProfiles() const noexcept;
|
||||
Model::ActionMap ActionMap() const noexcept;
|
||||
winrt::Windows::Foundation::Collections::IVectorView<Model::ExtensionPackage> Extensions() const noexcept;
|
||||
void WriteSettingsToDisk();
|
||||
Json::Value ToJson() const;
|
||||
Model::Profile ProfileDefaults() const;
|
||||
@@ -184,6 +216,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
|
||||
winrt::com_ptr<implementation::Profile> _baseLayerProfile = winrt::make_self<implementation::Profile>();
|
||||
winrt::Windows::Foundation::Collections::IObservableVector<Model::Profile> _allProfiles = winrt::single_threaded_observable_vector<Model::Profile>();
|
||||
winrt::Windows::Foundation::Collections::IObservableVector<Model::Profile> _activeProfiles = winrt::single_threaded_observable_vector<Model::Profile>();
|
||||
winrt::Windows::Foundation::Collections::IVector<Model::ExtensionPackage> _extensionPackages = winrt::single_threaded_vector<Model::ExtensionPackage>();
|
||||
std::set<std::string> _themesChangeLog{};
|
||||
|
||||
// load errors
|
||||
@@ -199,6 +232,70 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
|
||||
mutable std::once_flag _commandLinesCacheOnce;
|
||||
mutable std::vector<std::pair<std::wstring, Model::Profile>> _commandLinesCache;
|
||||
};
|
||||
|
||||
struct FragmentProfileEntry : FragmentProfileEntryT<FragmentProfileEntry>
|
||||
{
|
||||
public:
|
||||
FragmentProfileEntry(winrt::guid profileGuid, hstring json) :
|
||||
_profileGuid{ profileGuid },
|
||||
Json{ json } {}
|
||||
|
||||
winrt::guid ProfileGuid() const noexcept { return _profileGuid; }
|
||||
til::property<hstring> Json;
|
||||
|
||||
private:
|
||||
winrt::guid _profileGuid;
|
||||
};
|
||||
|
||||
struct FragmentColorSchemeEntry : FragmentColorSchemeEntryT<FragmentColorSchemeEntry>
|
||||
{
|
||||
public:
|
||||
FragmentColorSchemeEntry(hstring schemeName, hstring json) :
|
||||
_schemeName{ schemeName },
|
||||
_json{ json } {}
|
||||
|
||||
hstring ColorSchemeName() const noexcept { return _schemeName; }
|
||||
hstring Json() const noexcept { return _json; }
|
||||
WINRT_PROPERTY(bool, Conflict, false);
|
||||
|
||||
private:
|
||||
hstring _schemeName;
|
||||
hstring _json;
|
||||
};
|
||||
|
||||
struct FragmentSettings : FragmentSettingsT<FragmentSettings>
|
||||
{
|
||||
public:
|
||||
FragmentSettings(hstring source, hstring json, hstring jsonSource) :
|
||||
_source{ source },
|
||||
_Json{ json },
|
||||
_jsonSource{ jsonSource },
|
||||
_modifiedProfiles{ winrt::single_threaded_vector<Model::FragmentProfileEntry>() },
|
||||
_newProfiles{ winrt::single_threaded_vector<Model::FragmentProfileEntry>() },
|
||||
_colorSchemes{ winrt::single_threaded_vector<Model::FragmentColorSchemeEntry>() } {}
|
||||
|
||||
Model::FragmentSettings Copy() const;
|
||||
|
||||
hstring Source() const noexcept { return _source; }
|
||||
hstring JsonSource() const noexcept { return _jsonSource; }
|
||||
Windows::Foundation::Collections::IVector<Model::FragmentProfileEntry> ModifiedProfiles() const noexcept { return _modifiedProfiles; }
|
||||
Windows::Foundation::Collections::IVector<Model::FragmentProfileEntry> NewProfiles() const noexcept { return _newProfiles; }
|
||||
Windows::Foundation::Collections::IVector<Model::FragmentColorSchemeEntry> ColorSchemes() const noexcept { return _colorSchemes; }
|
||||
WINRT_PROPERTY(hstring, Json);
|
||||
|
||||
public:
|
||||
// views
|
||||
Windows::Foundation::Collections::IVectorView<Model::FragmentProfileEntry> ModifiedProfilesView() const noexcept { return _modifiedProfiles.GetView(); }
|
||||
Windows::Foundation::Collections::IVectorView<Model::FragmentProfileEntry> NewProfilesView() const noexcept { return _newProfiles.GetView(); }
|
||||
Windows::Foundation::Collections::IVectorView<Model::FragmentColorSchemeEntry> ColorSchemesView() const noexcept { return _colorSchemes.GetView(); }
|
||||
|
||||
private:
|
||||
hstring _source;
|
||||
hstring _jsonSource;
|
||||
Windows::Foundation::Collections::IVector<Model::FragmentProfileEntry> _modifiedProfiles;
|
||||
Windows::Foundation::Collections::IVector<Model::FragmentProfileEntry> _newProfiles;
|
||||
Windows::Foundation::Collections::IVector<Model::FragmentColorSchemeEntry> _colorSchemes;
|
||||
};
|
||||
}
|
||||
|
||||
namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation
|
||||
|
||||
@@ -8,6 +8,12 @@ import "DefaultTerminal.idl";
|
||||
|
||||
namespace Microsoft.Terminal.Settings.Model
|
||||
{
|
||||
enum FragmentScope
|
||||
{
|
||||
User,
|
||||
Machine
|
||||
};
|
||||
|
||||
[default_interface] runtimeclass CascadiaSettings {
|
||||
static CascadiaSettings LoadDefaults();
|
||||
static CascadiaSettings LoadAll();
|
||||
@@ -38,6 +44,7 @@ namespace Microsoft.Terminal.Settings.Model
|
||||
Profile DuplicateProfile(Profile sourceProfile);
|
||||
|
||||
ActionMap ActionMap { get; };
|
||||
Windows.Foundation.Collections.IVectorView<ExtensionPackage> Extensions { get; };
|
||||
|
||||
IVectorView<SettingsLoadWarnings> Warnings { get; };
|
||||
Windows.Foundation.IReference<SettingsLoadErrors> GetLoadingError { get; };
|
||||
@@ -56,4 +63,36 @@ namespace Microsoft.Terminal.Settings.Model
|
||||
|
||||
void ExpandCommands();
|
||||
}
|
||||
|
||||
[default_interface] runtimeclass FragmentProfileEntry
|
||||
{
|
||||
Guid ProfileGuid { get; };
|
||||
String Json { get; };
|
||||
}
|
||||
|
||||
[default_interface] runtimeclass FragmentColorSchemeEntry
|
||||
{
|
||||
String ColorSchemeName { get; };
|
||||
String Json { get; };
|
||||
Boolean Conflict { get; };
|
||||
}
|
||||
|
||||
[default_interface] runtimeclass FragmentSettings
|
||||
{
|
||||
String Source { get; };
|
||||
String Json { get; };
|
||||
String JsonSource { get; };
|
||||
IVectorView<FragmentProfileEntry> ModifiedProfilesView { get; };
|
||||
IVectorView<FragmentProfileEntry> NewProfilesView { get; };
|
||||
IVectorView<FragmentColorSchemeEntry> ColorSchemesView { get; };
|
||||
}
|
||||
|
||||
[default_interface] runtimeclass ExtensionPackage
|
||||
{
|
||||
String Source { get; };
|
||||
String DisplayName { get; };
|
||||
String Icon { get; };
|
||||
FragmentScope Scope { get; };
|
||||
IVectorView<FragmentSettings> FragmentsView { get; };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#if TIL_FEATURE_DYNAMICSSHPROFILES_ENABLED
|
||||
#include "SshHostGenerator.h"
|
||||
#endif
|
||||
#include "PowershellInstallationProfileGenerator.h"
|
||||
|
||||
#include "ApplicationState.h"
|
||||
#include "DefaultTerminal.h"
|
||||
@@ -178,15 +179,87 @@ 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);
|
||||
|
||||
const auto isPowerShellInstalled = !powerShellGenerator.GetPowerShellInstances().empty();
|
||||
if (!isPowerShellInstalled)
|
||||
{
|
||||
// Only generate the installer stub profile if PowerShell isn't installed.
|
||||
PowershellInstallationProfileGenerator pwshInstallationGenerator{};
|
||||
_executeGenerator(pwshInstallationGenerator);
|
||||
}
|
||||
|
||||
// Regardless of running the installer's generator, we need to do some cleanup still.
|
||||
_cleanupPowerShellInstaller(isPowerShellInstalled);
|
||||
}
|
||||
|
||||
WslDistroGenerator wslGenerator{};
|
||||
_executeGenerator(wslGenerator);
|
||||
|
||||
AzureCloudShellGenerator acsGenerator{};
|
||||
_executeGenerator(acsGenerator);
|
||||
|
||||
VisualStudioGenerator vsGenerator{};
|
||||
_executeGenerator(vsGenerator);
|
||||
|
||||
#if TIL_FEATURE_DYNAMICSSHPROFILES_ENABLED
|
||||
_executeGenerator(SshHostGenerator{});
|
||||
SshHostGenerator sshGenerator{};
|
||||
_executeGenerator(sshGenerator);
|
||||
#endif
|
||||
}
|
||||
|
||||
// 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<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()));
|
||||
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<FragmentProfileEntry>(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:
|
||||
// 1. The default profile is a PowerShell 7+ one, if one was generated,
|
||||
// and falls back to the standard PowerShell 5 profile otherwise.
|
||||
@@ -246,7 +319,7 @@ void SettingsLoader::FindFragmentsAndMergeIntoUserSettings()
|
||||
{
|
||||
ParsedSettings fragmentSettings;
|
||||
|
||||
const auto parseAndLayerFragmentFiles = [&](const std::filesystem::path& path, const winrt::hstring& source) {
|
||||
const auto parseAndLayerFragmentFiles = [&](const std::filesystem::path& path, const winrt::hstring& source, FragmentScope scope, bool layerFragment) {
|
||||
for (const auto& fragmentExt : std::filesystem::directory_iterator{ path })
|
||||
{
|
||||
if (fragmentExt.path().extension() == jsonExtension)
|
||||
@@ -256,7 +329,7 @@ void SettingsLoader::FindFragmentsAndMergeIntoUserSettings()
|
||||
const auto content = til::io::read_file_as_utf8_string_if_exists(fragmentExt.path());
|
||||
if (!content.empty())
|
||||
{
|
||||
_parseFragment(source, content, fragmentSettings);
|
||||
_parseFragment(source, content, fragmentSettings, scope, fragmentExt.path().filename().wstring(), layerFragment);
|
||||
}
|
||||
}
|
||||
CATCH_LOG();
|
||||
@@ -278,9 +351,12 @@ void SettingsLoader::FindFragmentsAndMergeIntoUserSettings()
|
||||
const auto filename = fragmentExtFolder.path().filename();
|
||||
const auto& source = filename.native();
|
||||
|
||||
if (!_ignoredNamespaces.contains(std::wstring_view{ source }) && fragmentExtFolder.is_directory())
|
||||
if (fragmentExtFolder.is_directory())
|
||||
{
|
||||
parseAndLayerFragmentFiles(fragmentExtFolder.path(), winrt::hstring{ source });
|
||||
parseAndLayerFragmentFiles(fragmentExtFolder.path(),
|
||||
winrt::hstring{ source },
|
||||
rfid == FOLDERID_LocalAppData ? FragmentScope::User : FragmentScope::Machine, // scope
|
||||
!_ignoredNamespaces.contains(std::wstring_view{ source })); // layerFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -312,11 +388,8 @@ void SettingsLoader::FindFragmentsAndMergeIntoUserSettings()
|
||||
|
||||
for (const auto& ext : extensions)
|
||||
{
|
||||
const auto packageName = ext.Package().Id().FamilyName();
|
||||
if (_ignoredNamespaces.contains(std::wstring_view{ packageName }))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const auto& package = ext.Package();
|
||||
const auto packageName = package.Id().FamilyName();
|
||||
|
||||
// Likewise, getting the public folder from an extension is an async operation.
|
||||
auto foundFolder = extractValueFromTaskWithoutMainThreadAwait(ext.GetPublicFolderAsync());
|
||||
@@ -334,7 +407,16 @@ void SettingsLoader::FindFragmentsAndMergeIntoUserSettings()
|
||||
|
||||
if (std::filesystem::is_directory(path))
|
||||
{
|
||||
parseAndLayerFragmentFiles(path, packageName);
|
||||
// MSIX does not support machine-wide scope
|
||||
// See https://github.com/microsoft/winget-cli/discussions/1983
|
||||
parseAndLayerFragmentFiles(path,
|
||||
packageName,
|
||||
FragmentScope::User,
|
||||
!_ignoredNamespaces.contains(std::wstring_view{ packageName })); // layerFragment
|
||||
|
||||
auto extPkg = extensionPackageMap[packageName];
|
||||
extPkg->Icon(package.Logo().AbsoluteUri());
|
||||
extPkg->DisplayName(package.DisplayName());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -345,7 +427,7 @@ void SettingsLoader::FindFragmentsAndMergeIntoUserSettings()
|
||||
void SettingsLoader::MergeFragmentIntoUserSettings(const winrt::hstring& source, const std::string_view& content)
|
||||
{
|
||||
ParsedSettings fragmentSettings;
|
||||
_parseFragment(source, content, fragmentSettings);
|
||||
_parseFragment(source, content, fragmentSettings, FragmentScope::User, hstring{ L"filename.json" }, true);
|
||||
}
|
||||
|
||||
// Call this method before passing SettingsLoader to the CascadiaSettings constructor.
|
||||
@@ -724,10 +806,18 @@ void SettingsLoader::_parse(const OriginTag origin, const winrt::hstring& source
|
||||
|
||||
// Just like _parse, but is to be used for fragment files, which don't support anything but color
|
||||
// schemes and profiles. Additionally this function supports profiles which specify an "updates" key.
|
||||
void SettingsLoader::_parseFragment(const winrt::hstring& source, const std::string_view& content, ParsedSettings& settings)
|
||||
// - scope: The scope of the fragment file (user or machine).
|
||||
// - jsonFilename: The filename of the JSON file being parsed.
|
||||
// - applyToSettings: If true, the parsed settings will be applied to the user settings. Otherwise, load the fragment for the settings UI, but don't apply it.
|
||||
void SettingsLoader::_parseFragment(const winrt::hstring& source, const std::string_view& content, ParsedSettings& settings, FragmentScope scope, std::wstring_view jsonFilename, bool applyToSettings)
|
||||
{
|
||||
auto json = _parseJson(content);
|
||||
|
||||
Json::StreamWriterBuilder styledWriter;
|
||||
styledWriter["indentation"] = " ";
|
||||
styledWriter["commentStyle"] = "All";
|
||||
auto fragmentSettings = winrt::make_self<FragmentSettings>(source, hstring{ til::u8u16(Json::writeString(styledWriter, json.root)) }, hstring{ jsonFilename });
|
||||
|
||||
settings.clear();
|
||||
|
||||
{
|
||||
@@ -740,19 +830,26 @@ void SettingsLoader::_parseFragment(const winrt::hstring& source, const std::str
|
||||
if (const auto scheme = ColorScheme::FromJson(schemeJson))
|
||||
{
|
||||
scheme->Origin(OriginTag::Fragment);
|
||||
// Don't add the color scheme to the Fragment's GlobalSettings; that will
|
||||
// cause layering issues later. Add them to a staging area for later processing.
|
||||
// (search for STAGED COLORS to find the next step)
|
||||
settings.colorSchemes.emplace(scheme->Name(), std::move(scheme));
|
||||
if (applyToSettings)
|
||||
{
|
||||
// Don't add the color scheme to the Fragment's GlobalSettings; that will
|
||||
// cause layering issues later. Add them to a staging area for later processing.
|
||||
// (search for STAGED COLORS to find the next step)
|
||||
settings.colorSchemes.emplace(scheme->Name(), std::move(scheme));
|
||||
}
|
||||
fragmentSettings->ColorSchemes().Append(winrt::make<FragmentColorSchemeEntry>(scheme->Name(), hstring{ til::u8u16(Json::writeString(styledWriter, schemeJson)) }));
|
||||
}
|
||||
}
|
||||
CATCH_LOG()
|
||||
}
|
||||
|
||||
// Parse out actions from the fragment. Manually opt-out of keybinding
|
||||
// parsing - fragments shouldn't be allowed to bind actions to keys
|
||||
// directly. We may want to revisit circa GH#2205
|
||||
settings.globals->LayerActionsFrom(json.root, OriginTag::Fragment, false);
|
||||
if (applyToSettings)
|
||||
{
|
||||
// Parse out actions from the fragment. Manually opt-out of keybinding
|
||||
// parsing - fragments shouldn't be allowed to bind actions to keys
|
||||
// directly. We may want to revisit circa GH#2205
|
||||
settings.globals->LayerActionsFrom(json.root, OriginTag::Fragment, false);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
@@ -764,48 +861,75 @@ void SettingsLoader::_parseFragment(const winrt::hstring& source, const std::str
|
||||
{
|
||||
try
|
||||
{
|
||||
auto profile = _parseProfile(OriginTag::Fragment, source, profileJson);
|
||||
// GH#9962: Discard Guid-less, Name-less profiles, but...
|
||||
// allow ones with an Updates field, as those are special for fragments.
|
||||
// We need to make sure to only call Guid() if HasGuid() is true,
|
||||
// as Guid() will dynamically generate a return value otherwise.
|
||||
const auto guid = profile->HasGuid() ? profile->Guid() : profile->Updates();
|
||||
if (guid != winrt::guid{})
|
||||
auto profile = _parseProfile(OriginTag::Fragment, source, profileJson);
|
||||
if (const auto guid = profile->Guid(); profile->HasGuid() && guid != winrt::guid{})
|
||||
{
|
||||
_appendProfile(std::move(profile), guid, settings);
|
||||
if (applyToSettings)
|
||||
{
|
||||
_appendProfile(std::move(profile), guid, settings);
|
||||
}
|
||||
fragmentSettings->NewProfiles().Append(winrt::make<FragmentProfileEntry>(guid, hstring{ til::u8u16(Json::writeString(styledWriter, profileJson)) }));
|
||||
}
|
||||
else if (const auto guid = profile->Updates(); guid != winrt::guid{})
|
||||
{
|
||||
if (applyToSettings)
|
||||
{
|
||||
_appendProfile(std::move(profile), guid, settings);
|
||||
}
|
||||
fragmentSettings->ModifiedProfiles().Append(winrt::make<FragmentProfileEntry>(guid, hstring{ til::u8u16(Json::writeString(styledWriter, profileJson)) }));
|
||||
}
|
||||
}
|
||||
CATCH_LOG()
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& fragmentProfile : settings.profiles)
|
||||
if (applyToSettings)
|
||||
{
|
||||
if (const auto updates = fragmentProfile->Updates(); updates != winrt::guid{})
|
||||
for (const auto& fragmentProfile : settings.profiles)
|
||||
{
|
||||
if (const auto it = userSettings.profilesByGuid.find(updates); it != userSettings.profilesByGuid.end())
|
||||
if (const auto updates = fragmentProfile->Updates(); updates != winrt::guid{})
|
||||
{
|
||||
it->second->AddMostImportantParent(fragmentProfile);
|
||||
if (const auto it = userSettings.profilesByGuid.find(updates); it != userSettings.profilesByGuid.end())
|
||||
{
|
||||
it->second->AddMostImportantParent(fragmentProfile);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_addUserProfileParent(fragmentProfile);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
// STAGED COLORS are processed here: we merge them into the partially-loaded
|
||||
// settings directly so that we can resolve conflicts between user-generated
|
||||
// color schemes and fragment-originated ones.
|
||||
for (const auto& [_, fragmentColorScheme] : settings.colorSchemes)
|
||||
{
|
||||
_addUserProfileParent(fragmentProfile);
|
||||
if (!_addOrMergeUserColorScheme(fragmentColorScheme))
|
||||
{
|
||||
// Color scheme wasn't added because it conflicted with a non-user created scheme.
|
||||
// Mark the fragment's color scheme as conflicting.
|
||||
for (auto schemeEntry : fragmentSettings->ColorSchemes())
|
||||
{
|
||||
if (schemeEntry.ColorSchemeName() == fragmentColorScheme->Name())
|
||||
{
|
||||
get_self<FragmentColorSchemeEntry>(schemeEntry)->Conflict(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// STAGED COLORS are processed here: we merge them into the partially-loaded
|
||||
// settings directly so that we can resolve conflicts between user-generated
|
||||
// color schemes and fragment-originated ones.
|
||||
for (const auto& fragmentColorScheme : settings.colorSchemes)
|
||||
{
|
||||
_addOrMergeUserColorScheme(fragmentColorScheme.second);
|
||||
// Add the parsed fragment globals as a parent of the user's settings.
|
||||
// Later, in FinalizeInheritance, this will result in the action map from
|
||||
// the fragments being applied before the user's own settings.
|
||||
userSettings.globals->AddLeastImportantParent(settings.globals);
|
||||
}
|
||||
|
||||
// Add the parsed fragment globals as a parent of the user's settings.
|
||||
// Later, in FinalizeInheritance, this will result in the action map from
|
||||
// the fragments being applied before the user's own settings.
|
||||
userSettings.globals->AddLeastImportantParent(settings.globals);
|
||||
_registerFragment(std::move(*fragmentSettings), scope);
|
||||
}
|
||||
|
||||
SettingsLoader::JsonSettings SettingsLoader::_parseJson(const std::string_view& content)
|
||||
@@ -905,7 +1029,8 @@ void SettingsLoader::_addUserProfileParent(const winrt::com_ptr<implementation::
|
||||
}
|
||||
}
|
||||
|
||||
void SettingsLoader::_addOrMergeUserColorScheme(const winrt::com_ptr<implementation::ColorScheme>& newScheme)
|
||||
// returns true if the scheme was successfully added, otherwise false
|
||||
bool SettingsLoader::_addOrMergeUserColorScheme(const winrt::com_ptr<implementation::ColorScheme>& newScheme)
|
||||
{
|
||||
// On entry, all the user color schemes have been loaded. Therefore, any insertions of inbox or fragment schemes
|
||||
// will fail; we can leverage this to detect when they are equivalent and delete the user's duplicate copies.
|
||||
@@ -931,41 +1056,83 @@ void SettingsLoader::_addOrMergeUserColorScheme(const winrt::com_ptr<implementat
|
||||
userSettings.colorSchemeRemappings.emplace(newScheme->Name(), newName);
|
||||
// And re-add it to the end.
|
||||
userSettings.colorSchemes.emplace(newName, std::move(existingScheme));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// As the name implies it executes a generator.
|
||||
// Generated profiles are added to .inboxSettings. Used by GenerateProfiles().
|
||||
void SettingsLoader::_executeGenerator(const IDynamicProfileGenerator& generator)
|
||||
void SettingsLoader::_executeGenerator(IDynamicProfileGenerator& generator)
|
||||
{
|
||||
const auto generatorNamespace = generator.GetNamespace();
|
||||
if (_ignoredNamespaces.contains(generatorNamespace))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const auto previousSize = inboxSettings.profiles.size();
|
||||
|
||||
std::vector<winrt::com_ptr<implementation::Profile>> generatedProfiles;
|
||||
try
|
||||
{
|
||||
generator.GenerateProfiles(inboxSettings.profiles);
|
||||
generator.GenerateProfiles(generatedProfiles);
|
||||
}
|
||||
CATCH_LOG_MSG("Dynamic Profile Namespace: \"%.*s\"", gsl::narrow<int>(generatorNamespace.size()), generatorNamespace.data())
|
||||
|
||||
// These are needed for the FragmentSettings object
|
||||
std::vector<Model::FragmentProfileEntry> profileEntries;
|
||||
Json::Value profilesListJson{ Json::ValueType::arrayValue };
|
||||
Json::StreamWriterBuilder styledWriter;
|
||||
styledWriter["indentation"] = " ";
|
||||
|
||||
// If the generator produced some profiles we're going to give them default attributes.
|
||||
// By setting the Origin/Source/etc. here, we deduplicate some code and ensure they aren't missing accidentally.
|
||||
if (inboxSettings.profiles.size() > previousSize)
|
||||
const winrt::hstring source{ generatorNamespace };
|
||||
for (const auto& profile : generatedProfiles)
|
||||
{
|
||||
const winrt::hstring source{ generatorNamespace };
|
||||
profile->Origin(OriginTag::Generated);
|
||||
profile->Source(source);
|
||||
|
||||
for (const auto& profile : std::span(inboxSettings.profiles).subspan(previousSize))
|
||||
const auto profileJson = profile->ToJson();
|
||||
profilesListJson.append(profileJson);
|
||||
profileEntries.push_back(winrt::make<FragmentProfileEntry>(profile->Guid(), hstring{ til::u8u16(Json::writeString(styledWriter, profileJson)) }));
|
||||
}
|
||||
|
||||
if (!_ignoredNamespaces.contains(generatorNamespace))
|
||||
{
|
||||
// Add generated profiles to the user settings
|
||||
for (auto& profile : generatedProfiles)
|
||||
{
|
||||
profile->Origin(OriginTag::Generated);
|
||||
profile->Source(source);
|
||||
inboxSettings.profiles.push_back(profile);
|
||||
}
|
||||
}
|
||||
|
||||
// Manually construct the JSON for the FragmentSettings object
|
||||
Json::Value json{ Json::ValueType::objectValue };
|
||||
json[JsonKey(ProfilesKey)] = profilesListJson;
|
||||
|
||||
auto generatorExtension = winrt::make_self<FragmentSettings>(hstring{ generatorNamespace }, hstring{ til::u8u16(Json::writeString(styledWriter, json)) }, hstring{ L"settings.json" });
|
||||
for (const auto& entry : profileEntries)
|
||||
{
|
||||
generatorExtension->NewProfiles().Append(entry);
|
||||
}
|
||||
auto extPkg = _registerFragment(std::move(*generatorExtension), FragmentScope::Machine);
|
||||
extPkg->DisplayName(hstring{ generator.GetDisplayName() });
|
||||
extPkg->Icon(hstring{ generator.GetIcon() });
|
||||
}
|
||||
|
||||
winrt::com_ptr<ExtensionPackage> SettingsLoader::_registerFragment(const winrt::Microsoft::Terminal::Settings::Model::FragmentSettings& fragment, FragmentScope scope)
|
||||
{
|
||||
const auto src = fragment.Source();
|
||||
if (auto extPkg = extensionPackageMap[src])
|
||||
{
|
||||
extPkg->Fragments().Append(fragment);
|
||||
return extPkg;
|
||||
}
|
||||
else
|
||||
{
|
||||
auto newExtPkg = winrt::make_self<ExtensionPackage>(src, scope);
|
||||
newExtPkg->Fragments().Append(fragment);
|
||||
extensionPackageMap[src] = newExtPkg;
|
||||
return newExtPkg;
|
||||
}
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
@@ -1258,6 +1425,11 @@ CascadiaSettings::CascadiaSettings(SettingsLoader&& loader) :
|
||||
_warnings = winrt::single_threaded_vector(std::move(warnings));
|
||||
_themesChangeLog = std::move(loader.userSettings.themesChangeLog);
|
||||
|
||||
for (auto [_, extPkg] : loader.extensionPackageMap)
|
||||
{
|
||||
_extensionPackages.Append(extPkg->Copy());
|
||||
}
|
||||
|
||||
_resolveDefaultProfile();
|
||||
_resolveNewTabMenuProfiles();
|
||||
_validateSettings();
|
||||
@@ -1491,7 +1663,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
|
||||
|
||||
@@ -92,6 +92,14 @@ winrt::com_ptr<GlobalAppSettings> GlobalAppSettings::Copy() const
|
||||
globals->_NewTabMenu->Append(get_self<NewTabMenuEntry>(entry)->Copy());
|
||||
}
|
||||
}
|
||||
if (_DisabledProfileSources)
|
||||
{
|
||||
globals->_DisabledProfileSources = winrt::single_threaded_vector<hstring>();
|
||||
for (const auto& src : *_DisabledProfileSources)
|
||||
{
|
||||
globals->_DisabledProfileSources->Append(src);
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& parent : _parents)
|
||||
{
|
||||
|
||||
@@ -30,6 +30,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model
|
||||
public:
|
||||
virtual ~IDynamicProfileGenerator() = default;
|
||||
virtual std::wstring_view GetNamespace() const noexcept = 0;
|
||||
virtual void GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles) const = 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) = 0;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -15,334 +15,311 @@
|
||||
#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)
|
||||
namespace winrt::Microsoft::Terminal::Settings::Model
|
||||
{
|
||||
const std::filesystem::path root{ wil::ExpandEnvironmentStringsW<std::wstring>(directory.data()) };
|
||||
if (std::filesystem::exists(root))
|
||||
DEFINE_ENUM_FLAG_OPERATORS(PowershellCoreProfileGenerator::PowerShellFlags);
|
||||
|
||||
constexpr bool PowershellCoreProfileGenerator::PowerShellInstance::operator<(const PowerShellInstance& second) const
|
||||
{
|
||||
for (const auto& versionedDir : std::filesystem::directory_iterator(root))
|
||||
if (majorVersion != second.majorVersion)
|
||||
{
|
||||
const auto versionedPath = versionedDir.path();
|
||||
const auto executable = versionedPath / PWSH_EXE;
|
||||
if (std::filesystem::exists(executable))
|
||||
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
|
||||
{
|
||||
std::wstringstream namestream;
|
||||
namestream << L"PowerShell";
|
||||
|
||||
if (WI_IsFlagSet(flags, PowerShellFlags::Store))
|
||||
{
|
||||
if (WI_IsFlagSet(flags, PowerShellFlags::Preview))
|
||||
{
|
||||
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 });
|
||||
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();
|
||||
}
|
||||
|
||||
// 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<PowershellCoreProfileGenerator::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 ? 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<PowerShellInstance>& 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<PowershellCoreProfileGenerator::PowerShellInstance>& out)
|
||||
{
|
||||
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))
|
||||
wil::unique_cotaskmem_string localAppDataFolder;
|
||||
if (FAILED(SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &localAppDataFolder)))
|
||||
{
|
||||
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 });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// App execution aliases for stable powershell
|
||||
const auto gaPath = appExecAliasPath / POWERSHELL_PFN;
|
||||
if (std::filesystem::exists(gaPath))
|
||||
std::filesystem::path appExecAliasPath{ localAppDataFolder.get() };
|
||||
appExecAliasPath /= L"Microsoft";
|
||||
appExecAliasPath /= L"WindowsApps";
|
||||
|
||||
if (std::filesystem::exists(appExecAliasPath))
|
||||
{
|
||||
const auto gaPackage = _getStorePackage(POWERSHELL_PFN);
|
||||
if (gaPackage)
|
||||
// App execution aliases for preview powershell
|
||||
const auto previewPath = appExecAliasPath / POWERSHELL_PREVIEW_PFN;
|
||||
if (std::filesystem::exists(previewPath))
|
||||
{
|
||||
out.emplace_back(PowerShellInstance{
|
||||
gaPackage.Id().Version().Major,
|
||||
PowerShellFlags::Store,
|
||||
gaPath / PWSH_EXE,
|
||||
});
|
||||
const auto previewPackage = _getStorePackage(POWERSHELL_PREVIEW_PFN);
|
||||
if (previewPackage)
|
||||
{
|
||||
out.emplace_back(PowershellCoreProfileGenerator::PowerShellInstance{
|
||||
gsl::narrow_cast<int>(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)
|
||||
{
|
||||
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<PowerShellInstance>& out)
|
||||
{
|
||||
const std::filesystem::path root{ wil::ExpandEnvironmentStringsW<std::wstring>(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<PowershellCoreProfileGenerator::PowerShellInstance>& out)
|
||||
{
|
||||
out.emplace_back(PowerShellInstance{ 0 /* we can't tell */, flags, pwshPath });
|
||||
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(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<PowerShellInstance> _collectPowerShellInstances()
|
||||
{
|
||||
std::vector<PowerShellInstance> 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<PowershellCoreProfileGenerator::PowerShellInstance> _collectPowerShellInstances()
|
||||
{
|
||||
std::vector<PowershellCoreProfileGenerator::PowerShellInstance> 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;
|
||||
}
|
||||
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 } };
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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)
|
||||
std::wstring_view PowershellCoreProfileGenerator::GetNamespace() const noexcept
|
||||
{
|
||||
const auto name = psI.Name();
|
||||
auto profile{ CreateDynamicProfile(name) };
|
||||
return PowershellCoreGeneratorNamespace;
|
||||
}
|
||||
|
||||
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 });
|
||||
std::wstring_view PowershellCoreProfileGenerator::GetDisplayName() const noexcept
|
||||
{
|
||||
return RS_(L"PowershellCoreProfileGeneratorDisplayName");
|
||||
}
|
||||
|
||||
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 });
|
||||
std::wstring_view PowershellCoreProfileGenerator::GetIcon() const noexcept
|
||||
{
|
||||
return GENERATOR_POWERSHELL_ICON;
|
||||
}
|
||||
|
||||
if (first)
|
||||
// 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)
|
||||
{
|
||||
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) };
|
||||
|
||||
first = false;
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
profiles.emplace_back(std::move(profile));
|
||||
std::vector<PowershellCoreProfileGenerator::PowerShellInstance> 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;
|
||||
}
|
||||
|
||||
@@ -25,7 +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;
|
||||
void GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles) const 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) override;
|
||||
std::vector<PowerShellInstance> GetPowerShellInstances() noexcept;
|
||||
|
||||
private:
|
||||
std::vector<PowerShellInstance> _powerShellInstances;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
#include "pch.h"
|
||||
|
||||
#include "PowershellInstallationProfileGenerator.h"
|
||||
#include "DynamicProfileUtils.h"
|
||||
|
||||
#include <LibraryResources.h>
|
||||
|
||||
static constexpr std::wstring_view POWERSHELL_ICON{ L"ms-appx:///ProfileIcons/pwsh.png" };
|
||||
static constexpr std::wstring_view GENERATOR_POWERSHELL_ICON{ L"ms-appx:///ProfileGeneratorIcons/PowerShell.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 GENERATOR_POWERSHELL_ICON;
|
||||
}
|
||||
|
||||
void PowershellInstallationProfileGenerator::GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles)
|
||||
{
|
||||
auto profile{ CreateDynamicProfile(RS_(L"PowerShellInstallationProfileName")) };
|
||||
profile->Commandline(winrt::hstring{ fmt::format(FMT_COMPILE(L"cmd /k winget install --interactive --id Microsoft.PowerShell --source winget & echo. & echo {} & exit"), RS_(L"PowerShellInstallationInstallerGuidance")) });
|
||||
profile->Icon(winrt::hstring{ POWERSHELL_ICON });
|
||||
profile->CloseOnExit(CloseOnExitMode::Never);
|
||||
|
||||
profiles.emplace_back(std::move(profile));
|
||||
}
|
||||
}
|
||||
@@ -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<winrt::com_ptr<implementation::Profile>>& profiles) override;
|
||||
};
|
||||
};
|
||||
@@ -740,4 +740,39 @@
|
||||
<data name="OpenCWDCommandKey" xml:space="preserve">
|
||||
<value>Open current working directory</value>
|
||||
</data>
|
||||
<data name="WslDistroGeneratorDisplayName" xml:space="preserve">
|
||||
<value>WSL Distro Profile Generator</value>
|
||||
<comment>The display name of a dynamic profile generator for WSL distros</comment>
|
||||
</data>
|
||||
<data name="PowershellCoreProfileGeneratorDisplayName" xml:space="preserve">
|
||||
<value>PowerShell Profile Generator</value>
|
||||
<comment>The display name of a dynamic profile generator for PowerShell</comment>
|
||||
</data>
|
||||
<data name="PowershellInstallationProfileGeneratorDisplayName" xml:space="preserve">
|
||||
<value>PowerShell Installation Generator</value>
|
||||
<comment>The display name of a dynamic profile generator that installs the latest PowerShell</comment>
|
||||
</data>
|
||||
<data name="PowershellInstallationProfileName" xml:space="preserve">
|
||||
<value>Install Latest PowerShell</value>
|
||||
<comment>The display name of a profile generated by the PowerShellInstallationProfileGenerator. This profile installs the latest PowerShell.</comment>
|
||||
</data>
|
||||
<data name="AzureCloudShellGeneratorDisplayName" xml:space="preserve">
|
||||
<value>Azure Cloud Shell Profile Generator</value>
|
||||
<comment>The display name of a dynamic profile generator for Azure Cloud Shell</comment>
|
||||
</data>
|
||||
<data name="VisualStudioGeneratorDisplayName" xml:space="preserve">
|
||||
<value>Visual Studio Profile Generator</value>
|
||||
<comment>The display name of a dynamic profile generator for Visual Studio</comment>
|
||||
</data>
|
||||
<data name="SshHostGeneratorDisplayName" xml:space="preserve">
|
||||
<value>SSH Host Profile Generator</value>
|
||||
<comment>The display name of a dynamic profile generator for SSH hosts</comment>
|
||||
</data>
|
||||
<data name="PowerShellInstallationInstallerGuidance" xml:space="preserve">
|
||||
<value>Restart Windows Terminal to apply the new profile.</value>
|
||||
<comment>Guidance displayed by the installer directing the user to restart the app.</comment>
|
||||
</data>
|
||||
<data name="PowerShellInstallationProfileJsonComment" xml:space="preserve">
|
||||
<value>This profile only appears if PowerShell is not installed</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -7,11 +7,13 @@
|
||||
#include "../../inc/DefaultSettings.h"
|
||||
|
||||
#include "DynamicProfileUtils.h"
|
||||
#include <LibraryResources.h>
|
||||
|
||||
static constexpr std::wstring_view SshHostGeneratorNamespace{ L"Windows.Terminal.SSH" };
|
||||
|
||||
static constexpr std::wstring_view PROFILE_TITLE_PREFIX = L"SSH - ";
|
||||
static constexpr std::wstring_view PROFILE_ICON_PATH = L"ms-appx:///ProfileIcons/{550ce7b8-d500-50ad-8a1a-c400c3262db3}.png";
|
||||
static constexpr std::wstring_view GENERATOR_ICON_PATH = L"ms-appx:///ProfileGeneratorIcons/SSH.png";
|
||||
|
||||
// OpenSSH is installed under System32 when installed via Optional Features
|
||||
static constexpr std::wstring_view SSH_EXE_PATH1 = L"%SystemRoot%\\System32\\OpenSSH\\ssh.exe";
|
||||
@@ -132,13 +134,23 @@ std::wstring_view SshHostGenerator::GetNamespace() const noexcept
|
||||
return SshHostGeneratorNamespace;
|
||||
}
|
||||
|
||||
std::wstring_view SshHostGenerator::GetDisplayName() const noexcept
|
||||
{
|
||||
return RS_(L"SshHostGeneratorDisplayName");
|
||||
}
|
||||
|
||||
std::wstring_view SshHostGenerator::GetIcon() const noexcept
|
||||
{
|
||||
return GENERATOR_ICON_PATH;
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Generate a list of profiles for each detected OpenSSH host.
|
||||
// Arguments:
|
||||
// - <none>
|
||||
// Return Value:
|
||||
// - <A list of SSH host profiles.>
|
||||
void SshHostGenerator::GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles) const
|
||||
void SshHostGenerator::GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles)
|
||||
{
|
||||
std::wstring sshExePath;
|
||||
if (_tryFindSshExePath(sshExePath))
|
||||
|
||||
@@ -24,7 +24,9 @@ namespace winrt::Microsoft::Terminal::Settings::Model
|
||||
{
|
||||
public:
|
||||
std::wstring_view GetNamespace() const noexcept override;
|
||||
void GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles) const 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) override;
|
||||
|
||||
private:
|
||||
static const std::wregex _configKeyValueRegex;
|
||||
|
||||
@@ -6,17 +6,29 @@
|
||||
#include "VisualStudioGenerator.h"
|
||||
#include "VsDevCmdGenerator.h"
|
||||
#include "VsDevShellGenerator.h"
|
||||
#include <LibraryResources.h>
|
||||
|
||||
using namespace winrt::Microsoft::Terminal::Settings::Model;
|
||||
|
||||
std::wstring_view VisualStudioGenerator::Namespace{ L"Windows.Terminal.VisualStudio" };
|
||||
static constexpr std::wstring_view IconPath{ L"ms-appx:///ProfileGeneratorIcons/VisualStudio.png" };
|
||||
|
||||
std::wstring_view VisualStudioGenerator::GetNamespace() const noexcept
|
||||
{
|
||||
return Namespace;
|
||||
}
|
||||
|
||||
void VisualStudioGenerator::GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles) const
|
||||
std::wstring_view VisualStudioGenerator::GetDisplayName() const noexcept
|
||||
{
|
||||
return RS_(L"VisualStudioGeneratorDisplayName");
|
||||
}
|
||||
|
||||
std::wstring_view VisualStudioGenerator::GetIcon() const noexcept
|
||||
{
|
||||
return IconPath;
|
||||
}
|
||||
|
||||
void VisualStudioGenerator::GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles)
|
||||
{
|
||||
const auto instances = VsSetupConfiguration::QueryInstances();
|
||||
|
||||
|
||||
@@ -28,7 +28,9 @@ namespace winrt::Microsoft::Terminal::Settings::Model
|
||||
public:
|
||||
static std::wstring_view Namespace;
|
||||
std::wstring_view GetNamespace() const noexcept override;
|
||||
void GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles) const 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) override;
|
||||
|
||||
class IVisualStudioProfileGenerator
|
||||
{
|
||||
|
||||
@@ -8,10 +8,13 @@
|
||||
#include "../../inc/DefaultSettings.h"
|
||||
|
||||
#include "DynamicProfileUtils.h"
|
||||
#include <LibraryResources.h>
|
||||
|
||||
static constexpr std::wstring_view WslHomeDirectory{ L"~" };
|
||||
static constexpr std::wstring_view DockerDistributionPrefix{ L"docker-desktop" };
|
||||
static constexpr std::wstring_view RancherDistributionPrefix{ L"rancher-desktop" };
|
||||
static constexpr std::wstring_view IconPath{ L"ms-appx:///ProfileIcons/{9acb9455-ca41-5af7-950f-6bca1bc9722f}.png" };
|
||||
static constexpr std::wstring_view GeneratorIconPath{ L"ms-appx:///ProfileGeneratorIcons/WSL.png" };
|
||||
|
||||
// The WSL entries are structured as such:
|
||||
// HKCU\Software\Microsoft\Windows\CurrentVersion\Lxss
|
||||
@@ -47,6 +50,16 @@ std::wstring_view WslDistroGenerator::GetNamespace() const noexcept
|
||||
return WslGeneratorNamespace;
|
||||
}
|
||||
|
||||
std::wstring_view WslDistroGenerator::GetDisplayName() const noexcept
|
||||
{
|
||||
return RS_(L"WslDistroGeneratorDisplayName");
|
||||
}
|
||||
|
||||
std::wstring_view WslDistroGenerator::GetIcon() const noexcept
|
||||
{
|
||||
return GeneratorIconPath;
|
||||
}
|
||||
|
||||
static winrt::com_ptr<implementation::Profile> makeProfile(const std::wstring& distName)
|
||||
{
|
||||
const auto WSLDistro{ CreateDynamicProfile(distName) };
|
||||
@@ -65,7 +78,7 @@ static winrt::com_ptr<implementation::Profile> makeProfile(const std::wstring& d
|
||||
{
|
||||
WSLDistro->StartingDirectory(winrt::hstring{ DEFAULT_STARTING_DIRECTORY });
|
||||
}
|
||||
WSLDistro->Icon(L"ms-appx:///ProfileIcons/{9acb9455-ca41-5af7-950f-6bca1bc9722f}.png");
|
||||
WSLDistro->Icon(winrt::hstring{ IconPath });
|
||||
WSLDistro->PathTranslationStyle(winrt::Microsoft::Terminal::Control::PathTranslationStyle::WSL);
|
||||
return WSLDistro;
|
||||
}
|
||||
@@ -226,7 +239,7 @@ static bool getWslNames(const wil::unique_hkey& wslRootKey,
|
||||
// - <none>
|
||||
// Return Value:
|
||||
// - A list of WSL profiles.
|
||||
void WslDistroGenerator::GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles) const
|
||||
void WslDistroGenerator::GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles)
|
||||
{
|
||||
auto wslRootKey{ openWslRegKey() };
|
||||
if (wslRootKey)
|
||||
|
||||
@@ -24,6 +24,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model
|
||||
{
|
||||
public:
|
||||
std::wstring_view GetNamespace() const noexcept override;
|
||||
void GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles) const 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) override;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -100,6 +100,11 @@
|
||||
<description>Enables the dynamic profile generator for OpenSSH config files</description>
|
||||
<id>9031</id>
|
||||
<stage>AlwaysDisabled</stage>
|
||||
<alwaysEnabledBrandingTokens>
|
||||
<brandingToken>Dev</brandingToken>
|
||||
<brandingToken>Canary</brandingToken>
|
||||
<brandingToken>Preview</brandingToken>
|
||||
</alwaysEnabledBrandingTokens>
|
||||
</feature>
|
||||
|
||||
<feature>
|
||||
|
||||
Reference in New Issue
Block a user