diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt
index cd757f1552..1223683f08 100644
--- a/.github/actions/spell-check/expect.txt
+++ b/.github/actions/spell-check/expect.txt
@@ -516,6 +516,7 @@ FARPROC
fdx
fesf
FFFF
+Figma
FILEEXPLORER
fileexploreraddons
fileexplorerpreview
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/ReloadFinishedMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/ReloadFinishedMessage.cs
new file mode 100644
index 0000000000..a6e2122edd
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/ReloadFinishedMessage.cs
@@ -0,0 +1,7 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Microsoft.CmdPal.UI.ViewModels.Messages;
+
+public record ReloadFinishedMessage();
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.Designer.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.Designer.cs
index e3a4e31088..be9d103b2d 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.Designer.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.Designer.cs
@@ -410,5 +410,23 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
return ResourceManager.GetString("builtin_reload_subtitle", resourceCulture);
}
}
+
+ ///
+ /// Looks up a localized string similar to {0} extensions found.
+ ///
+ public static string builtin_settings_extension_n_extensions_found {
+ get {
+ return ResourceManager.GetString("builtin_settings_extension_n_extensions_found", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to {0} extensions installed.
+ ///
+ public static string builtin_settings_extension_n_extensions_installed {
+ get {
+ return ResourceManager.GetString("builtin_settings_extension_n_extensions_installed", resourceCulture);
+ }
+ }
}
}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.resx b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.resx
index 6279a0bfdc..f0f68b8582 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.resx
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.resx
@@ -236,4 +236,10 @@
Home
+
+ {0} extensions found
+
+
+ {0} extensions installed
+
\ No newline at end of file
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsExtensionsViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsExtensionsViewModel.cs
new file mode 100644
index 0000000000..8e14a83d19
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsExtensionsViewModel.cs
@@ -0,0 +1,144 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.Globalization;
+using System.Text;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using CommunityToolkit.Mvvm.Messaging;
+
+using Microsoft.CmdPal.Core.ViewModels.Messages;
+using Microsoft.CmdPal.UI.ViewModels.Messages;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+
+namespace Microsoft.CmdPal.UI.ViewModels;
+
+///
+/// Provides filtering over the list of provider settings view models.
+/// Intended to be used by the UI to bind a TextBox (SearchText) and an ItemsRepeater (FilteredProviders).
+///
+public partial class SettingsExtensionsViewModel : ObservableObject
+{
+ private static readonly CompositeFormat LabelNumberExtensionFound
+ = CompositeFormat.Parse(Properties.Resources.builtin_settings_extension_n_extensions_found!);
+
+ private static readonly CompositeFormat LabelNumberExtensionInstalled
+ = CompositeFormat.Parse(Properties.Resources.builtin_settings_extension_n_extensions_installed!);
+
+ private readonly ObservableCollection _source;
+ private readonly TaskScheduler _uiScheduler;
+
+ public ObservableCollection FilteredProviders { get; } = [];
+
+ private string _searchText = string.Empty;
+
+ public string SearchText
+ {
+ get => _searchText;
+ set
+ {
+ if (_searchText != value)
+ {
+ _searchText = value;
+ OnPropertyChanged();
+ ApplyFilter();
+ }
+ }
+ }
+
+ public string ItemCounterText
+ {
+ get
+ {
+ var hasQuery = !string.IsNullOrWhiteSpace(_searchText);
+ var count = hasQuery ? FilteredProviders.Count : _source.Count;
+ var format = hasQuery ? LabelNumberExtensionFound : LabelNumberExtensionInstalled;
+ return string.Format(CultureInfo.CurrentCulture, format, count);
+ }
+ }
+
+ public bool ShowManualReloadOverlay
+ {
+ get;
+ private set
+ {
+ if (field != value)
+ {
+ field = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ public bool ShowNoResultsPanel => !string.IsNullOrWhiteSpace(_searchText) && FilteredProviders.Count == 0;
+
+ public bool HasResults => !ShowNoResultsPanel;
+
+ public IRelayCommand ReloadExtensionsCommand { get; }
+
+ public SettingsExtensionsViewModel(ObservableCollection source, TaskScheduler uiScheduler)
+ {
+ _source = source;
+ _uiScheduler = uiScheduler;
+ _source.CollectionChanged += Source_CollectionChanged;
+ ApplyFilter();
+
+ ReloadExtensionsCommand = new RelayCommand(ReloadExtensions);
+
+ WeakReferenceMessenger.Default.Register(this, (_, _) =>
+ {
+ Task.Factory.StartNew(() => ShowManualReloadOverlay = false, CancellationToken.None, TaskCreationOptions.None, _uiScheduler);
+ });
+ }
+
+ private void Source_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
+ {
+ ApplyFilter();
+ }
+
+ private void ApplyFilter()
+ {
+ var query = _searchText;
+ var filtered = ListHelpers.FilterList(_source, query, Matches);
+ ListHelpers.InPlaceUpdateList(FilteredProviders, filtered);
+ OnPropertyChanged(nameof(ItemCounterText));
+ OnPropertyChanged(nameof(HasResults));
+ OnPropertyChanged(nameof(ShowNoResultsPanel));
+ }
+
+ private static int Matches(string query, ProviderSettingsViewModel item)
+ {
+ if (string.IsNullOrWhiteSpace(query))
+ {
+ return 100;
+ }
+
+ return Contains(item.DisplayName, query)
+ || Contains(item.ExtensionName, query)
+ || Contains(item.ExtensionSubtext, query)
+ ? 100
+ : 0;
+ }
+
+ private static bool Contains(string? haystack, string needle)
+ {
+ return !string.IsNullOrEmpty(haystack) && haystack.Contains(needle, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [RelayCommand]
+ private void OpenStoreWithExtension(string? query)
+ {
+ const string extensionsAssocUri = "ms-windows-store://assoc/?Tags=AppExtension-com.microsoft.commandpalette";
+ ShellHelpers.OpenInShell(extensionsAssocUri);
+ }
+
+ private void ReloadExtensions()
+ {
+ ShowManualReloadOverlay = true;
+ WeakReferenceMessenger.Default.Send();
+ WeakReferenceMessenger.Default.Send();
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsViewModel.cs
index 8e05660656..380f2340ba 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsViewModel.cs
@@ -140,6 +140,8 @@ public partial class SettingsViewModel : INotifyPropertyChanged
public ObservableCollection CommandProviders { get; } = [];
+ public SettingsExtensionsViewModel Extensions { get; }
+
public SettingsViewModel(SettingsModel settings, IServiceProvider serviceProvider, TaskScheduler scheduler)
{
_settings = settings;
@@ -155,6 +157,8 @@ public partial class SettingsViewModel : INotifyPropertyChanged
var settingsModel = new ProviderSettingsViewModel(item, providerSettings, _serviceProvider);
CommandProviders.Add(settingsModel);
}
+
+ Extensions = new SettingsExtensionsViewModel(CommandProviders, scheduler);
}
private IEnumerable GetCommandProviders()
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs
index 5dd41280bf..c113b508d3 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs
@@ -259,6 +259,9 @@ public partial class TopLevelCommandManager : ObservableObject,
IsLoading = false;
+ // Send on the current thread; receivers should marshal to UI if needed
+ WeakReferenceMessenger.Default.Send();
+
return true;
}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/StoreLogo.dark.svg b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/StoreLogo.dark.svg
new file mode 100644
index 0000000000..ec3cb933d8
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/StoreLogo.dark.svg
@@ -0,0 +1,100 @@
+
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/StoreLogo.light.svg b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/StoreLogo.light.svg
new file mode 100644
index 0000000000..9d74683716
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/StoreLogo.light.svg
@@ -0,0 +1,100 @@
+
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/WinGetLogo.svg b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/WinGetLogo.svg
new file mode 100644
index 0000000000..0bf3ebbaff
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/WinGetLogo.svg
@@ -0,0 +1,97 @@
+
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj b/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj
index f577f13898..15bc3920fd 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj
@@ -181,6 +181,15 @@
PreserveNewest
+
+ Never
+
+
+ Never
+
+
+ Never
+
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionsPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionsPage.xaml
index f798134c10..9e9600bd6e 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionsPage.xaml
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionsPage.xaml
@@ -14,18 +14,204 @@
xmlns:viewModels="using:Microsoft.CmdPal.UI.ViewModels"
mc:Ignorable="d">
+
+
+
+
+
+
+
+
+
+ ms-appx:///Assets/StoreLogo.dark.svg
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ms-appx:///Assets/StoreLogo.light.svg
+
+
+
+
+ ms-appx:///Assets/StoreLogo.dark.svg
+
+
+
+
+
+
+
-
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionsPage.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionsPage.xaml.cs
index e164638ebb..e99296bad9 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionsPage.xaml.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionsPage.xaml.cs
@@ -8,6 +8,7 @@ using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Input;
namespace Microsoft.CmdPal.UI.Settings;
@@ -35,4 +36,10 @@ public sealed partial class ExtensionsPage : Page
}
}
}
+
+ private void OnFindInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args)
+ {
+ SearchBox?.Focus(FocusState.Keyboard);
+ args.Handled = true;
+ }
}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw b/src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw
index 4eb8be9b2b..506a879761 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw
@@ -474,4 +474,43 @@ Right-click to remove the key combination, thereby deactivating the shortcut.
Settings (Ctrl+,)
+
+ No extensions found
+
+
+ Try a different search term
+
+
+ More options
+
+
+ Reload extensions
+
+
+ Reloading extensions..
+
+
+ Discover more extensions
+
+
+ Find more extensions on the Microsoft Store or WinGet.
+
+
+ Learn how to create your own extensions
+
+
+ Find extensions on the Microsoft Store
+
+
+ Microsoft Store
+
+
+ Find extensions on WinGet
+
+
+ Microsoft Store
+
+
+ Search extensions
+
\ No newline at end of file