AllAppsCommandProvider.cs
1 // Copyright (c) Microsoft Corporation 2 // The Microsoft Corporation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 using System; 6 using System.Collections.Generic; 7 using Microsoft.CmdPal.Ext.Apps.Helpers; 8 using Microsoft.CmdPal.Ext.Apps.Programs; 9 using Microsoft.CmdPal.Ext.Apps.Properties; 10 using Microsoft.CmdPal.Ext.Apps.State; 11 using Microsoft.CommandPalette.Extensions; 12 using Microsoft.CommandPalette.Extensions.Toolkit; 13 14 namespace Microsoft.CmdPal.Ext.Apps; 15 16 public partial class AllAppsCommandProvider : CommandProvider 17 { 18 public const string WellKnownId = "AllApps"; 19 20 public static readonly AllAppsPage Page = new(); 21 22 private readonly AllAppsPage _page; 23 private readonly CommandItem _listItem; 24 25 public AllAppsCommandProvider() 26 : this(Page) 27 { 28 } 29 30 public AllAppsCommandProvider(AllAppsPage page) 31 { 32 _page = page ?? throw new ArgumentNullException(nameof(page)); 33 Id = WellKnownId; 34 DisplayName = Resources.installed_apps; 35 Icon = Icons.AllAppsIcon; 36 Settings = AllAppsSettings.Instance.Settings; 37 38 _listItem = new(_page) 39 { 40 MoreCommands = [new CommandContextItem(AllAppsSettings.Instance.Settings.SettingsPage)], 41 }; 42 43 // Subscribe to pin state changes to refresh the command provider 44 PinnedAppsManager.Instance.PinStateChanged += OnPinStateChanged; 45 } 46 47 public static int TopLevelResultLimit 48 { 49 get 50 { 51 var limitSetting = AllAppsSettings.Instance.SearchResultLimit; 52 53 if (limitSetting is null) 54 { 55 return 10; 56 } 57 58 var quantity = 10; 59 60 if (int.TryParse(limitSetting, out var result)) 61 { 62 quantity = result < 0 ? quantity : result; 63 } 64 65 return quantity; 66 } 67 } 68 69 public override ICommandItem[] TopLevelCommands() => [_listItem, .. _page.GetPinnedApps()]; 70 71 public ICommandItem? LookupAppByPackageFamilyName(string packageFamilyName, bool requireSingleMatch) 72 { 73 if (string.IsNullOrEmpty(packageFamilyName)) 74 { 75 return null; 76 } 77 78 var items = _page.GetItems(); 79 List<ICommandItem> matches = []; 80 81 foreach (var item in items) 82 { 83 if (item is AppListItem appItem && string.Equals(packageFamilyName, appItem.App.PackageFamilyName, StringComparison.OrdinalIgnoreCase)) 84 { 85 matches.Add(item); 86 if (!requireSingleMatch) 87 { 88 // Return early if we don't require uniqueness. 89 return item; 90 } 91 } 92 } 93 94 return requireSingleMatch && matches.Count == 1 ? matches[0] : null; 95 } 96 97 public ICommandItem? LookupAppByProductCode(string productCode, bool requireSingleMatch) 98 { 99 if (string.IsNullOrEmpty(productCode)) 100 { 101 return null; 102 } 103 104 if (!UninstallRegistryAppLocator.TryGetInstallInfo(productCode, out _, out var candidates) || candidates.Count <= 0) 105 { 106 return null; 107 } 108 109 var items = _page.GetItems(); 110 List<ICommandItem> matches = []; 111 112 foreach (var item in items) 113 { 114 if (item is not AppListItem appListItem || string.IsNullOrEmpty(appListItem.App.FullExecutablePath)) 115 { 116 continue; 117 } 118 119 foreach (var candidate in candidates) 120 { 121 if (string.Equals(appListItem.App.FullExecutablePath, candidate, StringComparison.OrdinalIgnoreCase)) 122 { 123 matches.Add(item); 124 if (!requireSingleMatch) 125 { 126 return item; 127 } 128 } 129 } 130 } 131 132 return requireSingleMatch && matches.Count == 1 ? matches[0] : null; 133 } 134 135 public ICommandItem? LookupAppByDisplayName(string displayName) 136 { 137 var items = _page.GetItems(); 138 139 var nameMatches = new List<ICommandItem>(); 140 ICommandItem? bestAppMatch = null; 141 var bestLength = -1; 142 143 foreach (var item in items) 144 { 145 if (item.Title is null) 146 { 147 continue; 148 } 149 150 // We're going to do this search in two directions: 151 // First, is this name a substring of any app... 152 if (item.Title.Contains(displayName)) 153 { 154 nameMatches.Add(item); 155 } 156 157 // ... Then, does any app have this name as a substring ... 158 // Only get one of these - "Terminal Preview" contains both "Terminal" and "Terminal Preview", so just take the best one 159 if (displayName.Contains(item.Title)) 160 { 161 if (item.Title.Length > bestLength) 162 { 163 bestLength = item.Title.Length; 164 bestAppMatch = item; 165 } 166 } 167 } 168 169 // ... Now, combine those two 170 List<ICommandItem> both = bestAppMatch is null ? nameMatches : [.. nameMatches, bestAppMatch]; 171 172 if (both.Count == 1) 173 { 174 return both[0]; 175 } 176 else if (nameMatches.Count == 1 && bestAppMatch is not null) 177 { 178 if (nameMatches[0] == bestAppMatch) 179 { 180 return nameMatches[0]; 181 } 182 } 183 184 return null; 185 } 186 187 private void OnPinStateChanged(object? sender, System.EventArgs e) 188 { 189 RaiseItemsChanged(0); 190 } 191 }