diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ExtensionOrderHelper.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ExtensionOrderHelper.cs
new file mode 100644
index 000000000000..124d57751dc8
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ExtensionOrderHelper.cs
@@ -0,0 +1,50 @@
+// 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;
+
+///
+/// Pure helper methods for sorting items according to a user-defined extension
+/// order. Operates on provider ID strings so the logic is easily testable without
+/// constructing heavyweight view-model instances.
+///
+internal static class ExtensionOrderHelper
+{
+ ///
+ /// Returns a new list with items sorted so that those whose provider is in
+ /// appear first (in that order), followed by
+ /// the remaining items in their original order. The sort is stable: items that
+ /// share a provider (and therefore the same order index) keep their original
+ /// relative order, so a single provider's commands are never shuffled.
+ ///
+ internal static List SortByExtensionOrder(List items, string[] extensionOrder, Func providerIdSelector)
+ {
+ var orderLookup = new Dictionary(extensionOrder.Length, StringComparer.Ordinal);
+ for (var i = 0; i < extensionOrder.Length; i++)
+ {
+ orderLookup.TryAdd(extensionOrder[i], i);
+ }
+
+ var ordered = new List(items.Count);
+ var unordered = new List(items.Count);
+
+ foreach (var item in items)
+ {
+ if (orderLookup.ContainsKey(providerIdSelector(item)))
+ {
+ ordered.Add(item);
+ }
+ else
+ {
+ unordered.Add(item);
+ }
+ }
+
+ // OrderBy is a stable sort, so commands from the same provider keep the
+ // relative order in which they were loaded.
+ var result = ordered.OrderBy(item => orderLookup[providerIdSelector(item)]).ToList();
+ result.AddRange(unordered);
+ return result;
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs
index cf3780e04f1e..1ec99699b828 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs
@@ -67,6 +67,14 @@ public string[] FallbackRanks
init => _fallbackRanks = value;
}
+ private string[]? _extensionOrder = [];
+
+ public string[] ExtensionOrder
+ {
+ get => _extensionOrder ?? [];
+ init => _extensionOrder = value;
+ }
+
private ImmutableDictionary? _aliases
= ImmutableDictionary.Empty;
@@ -165,12 +173,14 @@ public SettingsModel(
ImmutableList? pinnedCommands = null,
ImmutableDictionary? providerSettings = null,
string[]? fallbackRanks = null,
+ string[]? extensionOrder = null,
ImmutableDictionary? aliases = null,
ImmutableList? commandHotkeys = null)
{
PinnedCommands = pinnedCommands ?? ImmutableList.Empty;
ProviderSettings = providerSettings ?? ImmutableDictionary.Empty;
FallbackRanks = fallbackRanks ?? [];
+ ExtensionOrder = extensionOrder ?? [];
Aliases = aliases ?? ImmutableDictionary.Empty;
CommandHotkeys = commandHotkeys ?? ImmutableList.Empty;
}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsViewModel.cs
index 50256d077eb9..a5aa75bc43de 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsViewModel.cs
@@ -297,6 +297,8 @@ public bool EnableDock
public ObservableCollection FallbackRankings { get; set; } = new();
+ public ObservableCollection ExtensionOrderRankings { get; set; } = new();
+
public ObservableCollection MonitorConfigs { get; } = new();
public SettingsExtensionsViewModel Extensions { get; }
@@ -366,6 +368,24 @@ public SettingsViewModel(
}
FallbackRankings = new ObservableCollection(fallbackRankings.OrderBy(o => o.Score).Select(fr => fr.Item));
+
+ // Build extension order rankings: providers in ExtensionOrder first (in order), then the rest
+ var currentExtensionOrder = _settingsService.Settings.ExtensionOrder;
+ var extensionOrderLookup = new Dictionary(currentExtensionOrder.Length, StringComparer.Ordinal);
+ for (var i = 0; i < currentExtensionOrder.Length; i++)
+ {
+ extensionOrderLookup.TryAdd(currentExtensionOrder[i], i);
+ }
+
+ var orderedProviders = new List>(CommandProviders.Count);
+ foreach (var provider in CommandProviders)
+ {
+ var score = extensionOrderLookup.TryGetValue(provider.Id, out var idx) ? idx : CommandProviders.Count + orderedProviders.Count;
+ orderedProviders.Add(new Scored() { Item = provider, Score = score });
+ }
+
+ ExtensionOrderRankings = new ObservableCollection(orderedProviders.OrderBy(o => o.Score).Select(o => o.Item));
+
Extensions = new SettingsExtensionsViewModel(CommandProviders, scheduler);
if (needsSave)
@@ -393,6 +413,13 @@ public void ApplyFallbackSort()
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FallbackRankings)));
}
+ public void ApplyExtensionOrder()
+ {
+ _settingsService.UpdateSettings(s => s with { ExtensionOrder = ExtensionOrderRankings.Select(p => p.Id).ToArray() });
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ExtensionOrderRankings)));
+ WeakReferenceMessenger.Default.Send();
+ }
+
///
/// Builds or refreshes the collection by reconciling
/// connected monitors with persisted per-monitor settings.
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs
index 5fdc8c48f0a7..41ac04ddc44b 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs
@@ -191,9 +191,13 @@ private async Task UpdateCommandsForProvider(CommandProviderWrapper sender, IIte
var startIndex = FindIndexForFirstProviderItem(clone, sender.ProviderId);
clone.RemoveAll(item => item.CommandProviderId == sender.ProviderId);
- clone.InsertRange(startIndex, newItems);
- ListHelpers.InPlaceUpdateList(TopLevelCommands, clone);
+ // Insert the refreshed items where this provider previously sat (or at the
+ // end if it's brand new). RebuildTopLevelCommands then re-sorts the whole
+ // list so the provider lands in its user-configured position.
+ clone.InsertRange(Math.Min(startIndex, clone.Count), newItems);
+
+ RebuildTopLevelCommands(clone);
}
lock (_dockBandsLock)
@@ -469,10 +473,14 @@ private async Task RegisterAndLoadCommandsAsync(IEnumera
lock (TopLevelCommands)
{
- foreach (var c in commandsToAdd)
- {
- TopLevelCommands.Add(c);
- }
+ // Append the freshly loaded batch, then re-sort the entire top-level list
+ // so the user's configured extension order is honored across batches
+ // (built-ins and external extensions load in separate batches).
+ var clone = new List(TopLevelCommands.Count + commandsToAdd.Count);
+ clone.AddRange(TopLevelCommands);
+ clone.AddRange(commandsToAdd);
+
+ RebuildTopLevelCommands(clone);
}
lock (_dockBandsLock)
@@ -495,6 +503,23 @@ private async Task RegisterAndLoadCommandsAsync(IEnumera
return new RegisterAndLoadSummary(totalCommands, totalDockBands);
}
+ ///
+ /// Replaces the contents of with ,
+ /// first applying the user-configured extension order when one is set. Callers must hold the
+ /// lock. Built-in and external providers load in separate
+ /// batches, so re-sorting the whole list here is what lets the configured order span them.
+ ///
+ private void RebuildTopLevelCommands(List newCommands)
+ {
+ var extensionOrder = _serviceProvider.GetRequiredService().Settings.ExtensionOrder;
+ if (extensionOrder.Length > 0)
+ {
+ newCommands = ExtensionOrderHelper.SortByExtensionOrder(newCommands, extensionOrder, c => c.CommandProviderId);
+ }
+
+ ListHelpers.InPlaceUpdateList(TopLevelCommands, newCommands);
+ }
+
private async Task TryLoadCommandsAsync(CommandProviderWrapper wrapper, CancellationToken ct)
{
var sw = Stopwatch.StartNew();
@@ -539,10 +564,11 @@ private async Task AppendCommandsWhenReadyAsync(
{
lock (TopLevelCommands)
{
- foreach (var c in commands)
- {
- TopLevelCommands.Add(c);
- }
+ var clone = new List(TopLevelCommands.Count + commands.Count);
+ clone.AddRange(TopLevelCommands);
+ clone.AddRange(commands);
+
+ RebuildTopLevelCommands(clone);
}
}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ExtensionRanker.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ExtensionRanker.xaml
new file mode 100644
index 000000000000..42d6dc122e5a
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ExtensionRanker.xaml
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ExtensionRanker.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ExtensionRanker.xaml.cs
new file mode 100644
index 000000000000..a5a4cc02a125
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ExtensionRanker.xaml.cs
@@ -0,0 +1,31 @@
+// 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 Microsoft.CmdPal.UI.ViewModels;
+using Microsoft.CmdPal.UI.ViewModels.Services;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.UI.Xaml.Controls;
+
+namespace Microsoft.CmdPal.UI.Controls;
+
+public sealed partial class ExtensionRanker : UserControl
+{
+ private readonly TaskScheduler _mainTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
+ private SettingsViewModel? viewModel;
+
+ public ExtensionRanker()
+ {
+ this.InitializeComponent();
+
+ var topLevelCommandManager = App.Current.Services.GetService()!;
+ var themeService = App.Current.Services.GetService()!;
+ var settingsService = App.Current.Services.GetRequiredService();
+ viewModel = new SettingsViewModel(topLevelCommandManager, _mainTaskScheduler, themeService, settingsService);
+ }
+
+ private void ListView_DragItemsCompleted(ListViewBase sender, DragItemsCompletedEventArgs args)
+ {
+ viewModel?.ApplyExtensionOrder();
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ExtensionRankerDialog.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ExtensionRankerDialog.xaml
new file mode 100644
index 000000000000..a1b2face91cd
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ExtensionRankerDialog.xaml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+ 800
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ExtensionRankerDialog.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ExtensionRankerDialog.xaml.cs
new file mode 100644
index 000000000000..59d177d48807
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ExtensionRankerDialog.xaml.cs
@@ -0,0 +1,21 @@
+// 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 Microsoft.UI.Xaml.Controls;
+using Windows.Foundation;
+
+namespace Microsoft.CmdPal.UI.Controls;
+
+public sealed partial class ExtensionRankerDialog : UserControl
+{
+ public ExtensionRankerDialog()
+ {
+ InitializeComponent();
+ }
+
+ public IAsyncOperation ShowAsync()
+ {
+ return ExtensionRankerContentDialog!.ShowAsync()!;
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionsPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionsPage.xaml
index c8b11a88586d..4b21bc940ec2 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionsPage.xaml
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionsPage.xaml
@@ -178,6 +178,11 @@
+
+
+
+
+
+
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 2fb660db3d32..4a91687f4482 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionsPage.xaml.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionsPage.xaml.cs
@@ -93,4 +93,16 @@ private async void MenuFlyoutItem_OnClick(object sender, RoutedEventArgs e)
Logger.LogError("Error when showing FallbackRankerDialog", ex);
}
}
+
+ private async void ReorderExtensions_OnClick(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ await ExtensionRankerDialog!.ShowAsync();
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError("Error when showing ExtensionRankerDialog", ex);
+ }
+ }
}
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 1e7c91d3cddb..442f03faa7f2 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
@@ -823,6 +823,15 @@ Right-click to remove the key combination, thereby deactivating the shortcut.
Manage fallback order
+
+ Manage extension order
+
+
+ Manage extension order
+
+
+ Drag items to set which extensions appear first in the top-level list; extensions at the top take priority.
+
Version {0}
diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.UI.ViewModels.UnitTests/ExtensionOrderTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.UI.ViewModels.UnitTests/ExtensionOrderTests.cs
new file mode 100644
index 000000000000..6c2f1634d294
--- /dev/null
+++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.UI.ViewModels.UnitTests/ExtensionOrderTests.cs
@@ -0,0 +1,198 @@
+// 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.Generic;
+using System.Text.Json;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.CmdPal.UI.ViewModels.UnitTests;
+
+[TestClass]
+public class ExtensionOrderTests
+{
+ private static readonly string[] OrderAB = ["a", "b"];
+ private static readonly string[] OrderABC = ["a", "b", "c"];
+ private static readonly string[] ExpectedXYZ = ["x", "y", "z"];
+ private static readonly string[] ExpectedABC = ["a", "b", "c"];
+ private static readonly string[] ExpectedABXY = ["a", "b", "x", "y"];
+ private static readonly string[] OrderExternalThenBuiltIns = ["external.foo", "builtin.apps", "builtin.calc"];
+ private static readonly string[] OrderExternalThenApps = ["external.foo", "builtin.apps"];
+ private static readonly string[] ExpectedExternalFirst = ["external.foo", "builtin.apps", "builtin.calc"];
+ private static readonly string[] ExpectedNewProviderLast = ["external.foo", "builtin.apps", "newly.installed"];
+
+ [TestMethod]
+ public void ExtensionOrder_DefaultIsEmpty()
+ {
+ var settings = DeserializeSettings("{}");
+ Assert.IsNotNull(settings.ExtensionOrder);
+ Assert.AreEqual(0, settings.ExtensionOrder.Length);
+ }
+
+ [TestMethod]
+ public void ExtensionOrder_RoundTrips()
+ {
+ var order = new[] { "provider.b", "provider.a", "provider.c" };
+ var settings = DeserializeSettings("{}") with { ExtensionOrder = order };
+
+ var json = JsonSerializer.Serialize(settings, JsonSerializationContext.Default.SettingsModel);
+ var deserialized = JsonSerializer.Deserialize(json, JsonSerializationContext.Default.SettingsModel)!;
+
+ CollectionAssert.AreEqual(order, deserialized.ExtensionOrder);
+ }
+
+ [TestMethod]
+ public void ExtensionOrder_NullDeserializesToEmpty()
+ {
+ var json = """{"ExtensionOrder": null}""";
+ var settings = JsonSerializer.Deserialize(json, JsonSerializationContext.Default.SettingsModel)!;
+ Assert.IsNotNull(settings.ExtensionOrder);
+ Assert.AreEqual(0, settings.ExtensionOrder.Length);
+ }
+
+ [TestMethod]
+ public void ExtensionOrder_PreservesOrderInJson()
+ {
+ var order = new[] { "z.ext", "a.ext", "m.ext" };
+ var settings = DeserializeSettings("{}") with { ExtensionOrder = order };
+
+ var json = JsonSerializer.Serialize(settings, JsonSerializationContext.Default.SettingsModel);
+ var deserialized = JsonSerializer.Deserialize(json, JsonSerializationContext.Default.SettingsModel)!;
+
+ Assert.AreEqual("z.ext", deserialized.ExtensionOrder[0]);
+ Assert.AreEqual("a.ext", deserialized.ExtensionOrder[1]);
+ Assert.AreEqual("m.ext", deserialized.ExtensionOrder[2]);
+ }
+
+ [TestMethod]
+ public void SortByExtensionOrder_EmptyList_ReturnsEmpty()
+ {
+ var result = ExtensionOrderHelper.SortByExtensionOrder(
+ new List(),
+ OrderAB,
+ s => s);
+
+ Assert.AreEqual(0, result.Count);
+ }
+
+ [TestMethod]
+ public void SortByExtensionOrder_EmptyOrder_PreservesOriginalOrder()
+ {
+ var items = new List { "x", "y", "z" };
+ var result = ExtensionOrderHelper.SortByExtensionOrder(items, [], s => s);
+
+ CollectionAssert.AreEqual(ExpectedXYZ, result);
+ }
+
+ [TestMethod]
+ public void SortByExtensionOrder_AllInOrder_SortsAccordingly()
+ {
+ var items = new List { "c", "a", "b" };
+
+ var result = ExtensionOrderHelper.SortByExtensionOrder(items, OrderABC, s => s);
+
+ CollectionAssert.AreEqual(ExpectedABC, result);
+ }
+
+ [TestMethod]
+ public void SortByExtensionOrder_NoneInOrder_PreservesOriginalOrder()
+ {
+ var items = new List { "x", "y", "z" };
+
+ var result = ExtensionOrderHelper.SortByExtensionOrder(items, OrderABC, s => s);
+
+ CollectionAssert.AreEqual(ExpectedXYZ, result);
+ }
+
+ [TestMethod]
+ public void SortByExtensionOrder_Mixed_OrderedFirstThenUnordered()
+ {
+ var items = new List { "x", "b", "y", "a" };
+
+ var result = ExtensionOrderHelper.SortByExtensionOrder(items, OrderAB, s => s);
+
+ CollectionAssert.AreEqual(ExpectedABXY, result);
+ }
+
+ [TestMethod]
+ public void SortByExtensionOrder_DuplicateProviderIds_AllGroupedInOrder()
+ {
+ var items = new List { "b", "a", "b", "c", "a" };
+
+ var result = ExtensionOrderHelper.SortByExtensionOrder(items, OrderAB, s => s);
+
+ Assert.AreEqual("a", result[0]);
+ Assert.AreEqual("a", result[1]);
+ Assert.AreEqual("b", result[2]);
+ Assert.AreEqual("b", result[3]);
+ Assert.AreEqual("c", result[4]);
+ }
+
+ [TestMethod]
+ public void SortByExtensionOrder_SingleElement_ReturnsIt()
+ {
+ var items = new List { "a" };
+
+ var result = ExtensionOrderHelper.SortByExtensionOrder(items, OrderAB, s => s);
+
+ Assert.AreEqual(1, result.Count);
+ Assert.AreEqual("a", result[0]);
+ }
+
+ [TestMethod]
+ public void SortByExtensionOrder_SameProviderItems_KeepRelativeOrder()
+ {
+ // Two providers ("a" and "b"), each contributing several commands. The sort
+ // must be stable so a single provider's commands are never shuffled.
+ var items = new List<(string Provider, int Command)>
+ {
+ ("b", 0),
+ ("a", 0),
+ ("b", 1),
+ ("a", 1),
+ ("a", 2),
+ };
+
+ var result = ExtensionOrderHelper.SortByExtensionOrder(items, OrderAB, x => x.Provider);
+
+ var expected = new List<(string, int)>
+ {
+ ("a", 0),
+ ("a", 1),
+ ("a", 2),
+ ("b", 0),
+ ("b", 1),
+ };
+ CollectionAssert.AreEqual(expected, result);
+ }
+
+ [TestMethod]
+ public void SortByExtensionOrder_ExternalCanOutrankBuiltIn_WhenBothInOrder()
+ {
+ // Built-ins load before external extensions, but the configured order lists the
+ // external provider first. Sorting the full list lets the external outrank the
+ // built-in so the result matches what the reorder dialog showed (WYSIWYG).
+ var items = new List { "builtin.apps", "builtin.calc", "external.foo" };
+
+ var result = ExtensionOrderHelper.SortByExtensionOrder(items, OrderExternalThenBuiltIns, s => s);
+
+ CollectionAssert.AreEqual(ExpectedExternalFirst, result);
+ }
+
+ [TestMethod]
+ public void SortByExtensionOrder_NewProviderNotInOrder_GoesToEnd()
+ {
+ // A provider installed after the last reorder isn't in the saved order, so it
+ // keeps its natural load position at the end rather than jumping to the front.
+ var items = new List { "external.foo", "builtin.apps", "newly.installed" };
+
+ var result = ExtensionOrderHelper.SortByExtensionOrder(items, OrderExternalThenApps, s => s);
+
+ CollectionAssert.AreEqual(ExpectedNewProviderLast, result);
+ }
+
+ private static SettingsModel DeserializeSettings(string json)
+ {
+ return JsonSerializer.Deserialize(json, JsonSerializationContext.Default.SettingsModel) ?? new SettingsModel();
+ }
+}