/ src / modules / cmdpal / ext / Microsoft.CmdPal.Ext.Apps / AppListItem.cs
AppListItem.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 System.Threading.Tasks;
  8  using ManagedCommon;
  9  using Microsoft.CmdPal.Core.Common.Helpers;
 10  using Microsoft.CmdPal.Ext.Apps.Commands;
 11  using Microsoft.CmdPal.Ext.Apps.Helpers;
 12  using Microsoft.CommandPalette.Extensions;
 13  using Microsoft.CommandPalette.Extensions.Toolkit;
 14  
 15  namespace Microsoft.CmdPal.Ext.Apps.Programs;
 16  
 17  public sealed partial class AppListItem : ListItem
 18  {
 19      private readonly AppCommand _appCommand;
 20      private readonly AppItem _app;
 21  
 22      private readonly Lazy<Task<IconInfo?>> _iconLoadTask;
 23      private readonly Lazy<Task<Details>> _detailsLoadTask;
 24  
 25      private InterlockedBoolean _isLoadingIcon;
 26      private InterlockedBoolean _isLoadingDetails;
 27  
 28      public override IDetails? Details
 29      {
 30          get
 31          {
 32              if (_isLoadingDetails.Set())
 33              {
 34                  _ = LoadDetailsAsync();
 35              }
 36  
 37              return base.Details;
 38          }
 39          set => base.Details = value;
 40      }
 41  
 42      public override IIconInfo? Icon
 43      {
 44          get
 45          {
 46              if (_isLoadingIcon.Set())
 47              {
 48                  _ = LoadIconAsync();
 49              }
 50  
 51              return base.Icon;
 52          }
 53          set => base.Icon = value;
 54      }
 55  
 56      public string AppIdentifier => _app.AppIdentifier;
 57  
 58      public AppItem App => _app;
 59  
 60      public AppListItem(AppItem app, bool useThumbnails, bool isPinned)
 61      {
 62          Command = _appCommand = new AppCommand(app);
 63          _app = app;
 64          Title = app.Name;
 65          Subtitle = app.Subtitle;
 66          Icon = Icons.GenericAppIcon;
 67  
 68          MoreCommands = AddPinCommands(_app.Commands!, isPinned);
 69  
 70          _detailsLoadTask = new Lazy<Task<Details>>(BuildDetails);
 71          _iconLoadTask = new Lazy<Task<IconInfo?>>(async () => await FetchIcon(useThumbnails).ConfigureAwait(false));
 72      }
 73  
 74      private async Task LoadDetailsAsync()
 75      {
 76          try
 77          {
 78              Details = await _detailsLoadTask.Value;
 79          }
 80          catch (Exception ex)
 81          {
 82              Logger.LogWarning($"Failed to load details for {AppIdentifier}\n{ex}");
 83          }
 84      }
 85  
 86      private async Task LoadIconAsync()
 87      {
 88          try
 89          {
 90              Icon = _appCommand.Icon = CoalesceIcon(await _iconLoadTask.Value);
 91          }
 92          catch (Exception ex)
 93          {
 94              Logger.LogWarning($"Failed to load icon for {AppIdentifier}\n{ex}");
 95          }
 96      }
 97  
 98      private static IconInfo CoalesceIcon(IconInfo? value)
 99      {
100          return CoalesceIcon(value, Icons.GenericAppIcon)!;
101      }
102  
103      private static IconInfo? CoalesceIcon(IconInfo? value, IconInfo? replacement)
104      {
105          return IconIsNullOrEmpty(value) ? replacement : value;
106      }
107  
108      private static bool IconIsNullOrEmpty(IconInfo? value)
109      {
110          return value == null || (string.IsNullOrEmpty(value.Light?.Icon) && value.Light?.Data is null) || (string.IsNullOrEmpty(value.Dark?.Icon) && value.Dark?.Data is null);
111      }
112  
113      private async Task<Details> BuildDetails()
114      {
115          // Build metadata, with app type, path, etc.
116          var metadata = new List<DetailsElement>();
117          metadata.Add(new DetailsElement() { Key = "Type", Data = new DetailsTags() { Tags = [new Tag(_app.Type)] } });
118          if (!_app.IsPackaged)
119          {
120              metadata.Add(new DetailsElement() { Key = "Path", Data = new DetailsLink() { Text = _app.ExePath } });
121          }
122  
123  #if DEBUG
124          metadata.Add(new DetailsElement() { Key = "[DEBUG] AppIdentifier", Data = new DetailsLink() { Text = _app.AppIdentifier } });
125          metadata.Add(new DetailsElement() { Key = "[DEBUG] ExePath", Data = new DetailsLink() { Text = _app.ExePath } });
126          metadata.Add(new DetailsElement() { Key = "[DEBUG] IcoPath", Data = new DetailsLink() { Text = _app.IcoPath } });
127          metadata.Add(new DetailsElement() { Key = "[DEBUG] JumboIconPath", Data = new DetailsLink() { Text = _app.JumboIconPath ?? "(null)" } });
128  #endif
129  
130          // Icon
131          IconInfo? heroImage = null;
132          if (_app.IsPackaged)
133          {
134              heroImage = new IconInfo(_app.JumboIconPath ?? _app.IcoPath);
135          }
136          else
137          {
138              // Get the icon from the system
139              if (!string.IsNullOrEmpty(_app.JumboIconPath))
140              {
141                  var randomAccessStream = await IconExtractor.GetIconStreamAsync(_app.JumboIconPath, 64);
142                  if (randomAccessStream != null)
143                  {
144                      heroImage = IconInfo.FromStream(randomAccessStream);
145                  }
146              }
147  
148              if (IconIsNullOrEmpty(heroImage) && !string.IsNullOrEmpty(_app.IcoPath))
149              {
150                  var randomAccessStream = await IconExtractor.GetIconStreamAsync(_app.IcoPath, 64);
151                  if (randomAccessStream != null)
152                  {
153                      heroImage = IconInfo.FromStream(randomAccessStream);
154                  }
155              }
156  
157              // do nothing if we fail to load an icon.
158              // Logging it would be too NOISY, there's really no need.
159              if (IconIsNullOrEmpty(heroImage) && !string.IsNullOrEmpty(_app.JumboIconPath))
160              {
161                  heroImage = await TryLoadThumbnail(_app.JumboIconPath, jumbo: true, logOnFailure: false);
162              }
163  
164              if (IconIsNullOrEmpty(heroImage) && !string.IsNullOrEmpty(_app.IcoPath))
165              {
166                  heroImage = await TryLoadThumbnail(_app.IcoPath, jumbo: true, logOnFailure: false);
167              }
168  
169              if (IconIsNullOrEmpty(heroImage) && !string.IsNullOrEmpty(_app.ExePath))
170              {
171                  heroImage = await TryLoadThumbnail(_app.ExePath, jumbo: true, logOnFailure: false);
172              }
173          }
174  
175          return new Details()
176          {
177              Title = this.Title,
178              HeroImage = CoalesceIcon(CoalesceIcon(heroImage, this.Icon as IconInfo)),
179              Metadata = [..metadata],
180          };
181      }
182  
183      private async Task<IconInfo> FetchIcon(bool useThumbnails)
184      {
185          IconInfo? icon = null;
186          if (_app.IsPackaged)
187          {
188              icon = new IconInfo(_app.IcoPath);
189              return icon;
190          }
191  
192          if (useThumbnails)
193          {
194              if (!string.IsNullOrEmpty(_app.IcoPath))
195              {
196                  icon = await TryLoadThumbnail(_app.IcoPath, jumbo: false, logOnFailure: true);
197              }
198  
199              if (IconIsNullOrEmpty(icon) && !string.IsNullOrEmpty(_app.ExePath))
200              {
201                  icon = await TryLoadThumbnail(_app.ExePath, jumbo: false, logOnFailure: true);
202              }
203          }
204  
205          icon ??= new IconInfo(_app.IcoPath);
206  
207          return icon;
208      }
209  
210      private IContextItem[] AddPinCommands(List<IContextItem> commands, bool isPinned)
211      {
212          var newCommands = new List<IContextItem>();
213          newCommands.AddRange(commands);
214  
215          newCommands.Add(new Separator());
216  
217          if (isPinned)
218          {
219              newCommands.Add(
220                  new CommandContextItem(
221                      new UnpinAppCommand(this.AppIdentifier))
222                  {
223                      RequestedShortcut = KeyChords.TogglePin,
224                  });
225          }
226          else
227          {
228              newCommands.Add(
229                  new CommandContextItem(
230                      new PinAppCommand(this.AppIdentifier))
231                  {
232                      RequestedShortcut = KeyChords.TogglePin,
233                  });
234          }
235  
236          return newCommands.ToArray();
237      }
238  
239      private async Task<IconInfo?> TryLoadThumbnail(string path, bool jumbo, bool logOnFailure)
240      {
241          return await Task.Run(async () =>
242          {
243              try
244              {
245                  var stream = await ThumbnailHelper.GetThumbnail(path, jumbo).ConfigureAwait(false);
246                  if (stream is not null)
247                  {
248                      return IconInfo.FromStream(stream);
249                  }
250              }
251              catch (Exception ex)
252              {
253                  if (logOnFailure)
254                  {
255                      Logger.LogDebug($"Failed to load icon {path} for {AppIdentifier}:\n{ex}");
256                  }
257              }
258  
259              return null;
260          }).ConfigureAwait(false);
261      }
262  }