Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/actions/spell-check/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,7 @@ IGo
iid
Iindex
Ijwhost
ILD
IMAGEHLP
IMAGERESIZERCONTEXTMENU
IMAGERESIZEREXT
Expand Down Expand Up @@ -1453,13 +1454,17 @@ SHELLDLL
shellex
SHELLEXECUTEINFO
SHELLEXECUTEINFOW
SHELLEXTENSION
SHELLICONSIZE
SHELLNEWVALUE
SHFILEINFO
SHFILEOPSTRUCT
SHGDN
SHGDNF
SHGFI
SHGFIICON
SHGFILARGEICON
SHIL
shinfo
shlwapi
shobjidl
Expand Down
38 changes: 22 additions & 16 deletions src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsPage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ private void BuildListItems()
Stopwatch stopwatch = new();
stopwatch.Start();

List<AppItem> apps = GetPrograms();
var apps = GetPrograms();

this.allAppsSection = apps
.Select((app) => new AppListItem(app, true))
Expand All @@ -73,26 +73,15 @@ private void BuildListItems()

internal List<AppItem> GetPrograms()
{
IEnumerable<AppItem> uwpResults = AppCache.Instance.Value.UWPs
var uwpResults = AppCache.Instance.Value.UWPs
.Where((application) => application.Enabled)
.Select(app =>
new AppItem()
{
Name = app.Name,
Subtitle = app.Description,
Type = UWPApplication.Type(),
IcoPath = app.LogoType != LogoType.Error ? app.LogoPath : string.Empty,
DirPath = app.Location,
UserModelId = app.UserModelId,
IsPackaged = true,
Commands = app.GetCommands(),
});
.Select(UwpToAppItem);

IEnumerable<AppItem> win32Results = AppCache.Instance.Value.Win32s
var win32Results = AppCache.Instance.Value.Win32s
.Where((application) => application.Enabled && application.Valid)
.Select(app =>
{
string icoPath = string.IsNullOrEmpty(app.IcoPath) ?
var icoPath = string.IsNullOrEmpty(app.IcoPath) ?
(app.AppType == Win32Program.ApplicationType.InternetShortcutApplication ?
app.IcoPath :
app.FullPath) :
Expand All @@ -116,4 +105,21 @@ internal List<AppItem> GetPrograms()

return uwpResults.Concat(win32Results).OrderBy(app => app.Name).ToList();
}

private AppItem UwpToAppItem(UWPApplication app)
{
var iconPath = app.LogoType != LogoType.Error ? app.LogoPath : string.Empty;
var item = new AppItem()
{
Name = app.Name,
Subtitle = app.Description,
Type = UWPApplication.Type(),
IcoPath = iconPath,
DirPath = app.Location,
UserModelId = app.UserModelId,
IsPackaged = true,
Commands = app.GetCommands(),
};
return item;
}
}
46 changes: 33 additions & 13 deletions src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppListItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,13 @@ public AppListItem(AppItem app, bool useThumbnails)
Tags = [_appTag];
MoreCommands = _app.Commands!.ToArray();

_details = new Lazy<Details>(() => BuildDetails());
_details = new Lazy<Details>(() =>
{
var t = BuildDetails();
t.Wait();
return t.Result;
});

_icon = new Lazy<IconInfo>(() =>
{
var t = FetchIcon(useThumbnails);
Expand All @@ -41,19 +47,43 @@ public AppListItem(AppItem app, bool useThumbnails)
});
}

private Details BuildDetails()
private async Task<Details> BuildDetails()
{
// Build metadata, with app type, path, etc.
var metadata = new List<DetailsElement>();
metadata.Add(new DetailsElement() { Key = "Type", Data = new DetailsTags() { Tags = [new Tag(_app.Type)] } });
if (!_app.IsPackaged)
{
metadata.Add(new DetailsElement() { Key = "Path", Data = new DetailsLink() { Text = _app.ExePath } });
}

// Icon
IconInfo? heroImage = null;
if (_app.IsPackaged)
{
heroImage = new IconInfo(_app.IcoPath);
}
else
{
try
{
var stream = await ThumbnailHelper.GetThumbnail(_app.ExePath, true);
if (stream != null)
{
heroImage = IconInfo.FromStream(stream);
}
}
catch (Exception)
{
// do nothing if we fail to load an icon.
// Logging it would be too NOISY, there's really no need.
}
}

return new Details()
{
Title = this.Title,
HeroImage = this.Icon ?? new IconInfo(string.Empty),
HeroImage = heroImage ?? this.Icon ?? new IconInfo(string.Empty),
Metadata = metadata.ToArray(),
};
}
Expand All @@ -64,11 +94,6 @@ public async Task<IconInfo> FetchIcon(bool useThumbnails)
if (_app.IsPackaged)
{
icon = new IconInfo(_app.IcoPath);
if (_details.IsValueCreated)
{
_details.Value.HeroImage = icon;
}

return icon;
}

Expand All @@ -94,11 +119,6 @@ public async Task<IconInfo> FetchIcon(bool useThumbnails)
icon = new IconInfo(_app.IcoPath);
}

if (_details.IsValueCreated)
{
_details.Value.HeroImage = icon;
}

return icon;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@
using System.Collections.Generic;
using System.IO.Abstractions;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Xml;
using Microsoft.CmdPal.Ext.Apps.Commands;
using Microsoft.CmdPal.Ext.Apps.Properties;
using Microsoft.CmdPal.Ext.Apps.Utils;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using static Microsoft.CmdPal.Ext.Apps.Utils.Native;
using PackageVersion = Microsoft.CmdPal.Ext.Apps.Programs.UWP.PackageVersion;
Expand Down Expand Up @@ -314,7 +312,12 @@ private bool SetScaleIcons(string path, string colorscheme, bool highContrast =
}
}

var selectedIconPath = paths.FirstOrDefault(File.Exists);
// By working from the highest resolution to the lowest, we make
// sure that we use the highest quality possible icon for the app.
//
// FirstOrDefault would result in us using the 1x scaled icon
// always, which is usually too small for our needs.
var selectedIconPath = paths.LastOrDefault(File.Exists);
if (!string.IsNullOrEmpty(selectedIconPath))
{
LogoPath = selectedIconPath;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,10 @@ internal struct SHFILEINFO

[DllImport("user32.dll", CharSet = CharSet.Unicode)]
internal static extern bool DestroyIcon(IntPtr hIcon);

[DllImport("Shell32.dll", CharSet = CharSet.Unicode)]
internal static extern int SHGetImageList(int iImageList, ref Guid riid, out IntPtr ppv);

[DllImport("comctl32.dll", SetLastError = true)]
internal static extern int ImageList_GetIcon(IntPtr himl, int i, int flags);
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,12 @@ public class ThumbnailHelper
".ico",
];

public static Task<IRandomAccessStream?> GetThumbnail(string path)
public static Task<IRandomAccessStream?> GetThumbnail(string path, bool jumbo = false)
{
var extension = Path.GetExtension(path).ToLower(CultureInfo.InvariantCulture);
try
{
if (ImageExtensions.Contains(extension))
{
return GetImageThumbnailAsync(path);
}
else
{
return GetFileIconStream(path);
}
return ImageExtensions.Contains(extension) ? GetImageThumbnailAsync(path) : GetFileIconStream(path, jumbo);
}
catch (Exception)
{
Expand All @@ -45,9 +38,19 @@ public class ThumbnailHelper
return Task.FromResult<IRandomAccessStream?>(null);
}

private const uint SHGFIICON = 0x000000100;
private const uint SHGFILARGEICON = 0x000000000;

// these are windows constants and mangling them is goofy
#pragma warning disable SA1310 // Field names should not contain underscore
#pragma warning disable SA1306 // Field names should begin with lower-case letter
private const uint SHGFI_ICON = 0x000000100;
private const uint SHGFI_SHELLICONSIZE = 0x000000004;
private const int SHGFI_SYSICONINDEX = 0x000004000;
private const int SHIL_JUMBO = 4;
private const int ILD_TRANSPARENT = 1;
#pragma warning restore SA1306 // Field names should begin with lower-case letter
#pragma warning restore SA1310 // Field names should not contain underscore

// This will call DestroyIcon on the hIcon passed in.
// Duplicate it if you need it again after this.
private static MemoryStream GetMemoryStreamFromIcon(IntPtr hIcon)
{
var memoryStream = new MemoryStream();
Expand All @@ -65,19 +68,40 @@ private static MemoryStream GetMemoryStreamFromIcon(IntPtr hIcon)
return memoryStream;
}

private static async Task<IRandomAccessStream?> GetFileIconStream(string filePath)
private static async Task<IRandomAccessStream?> GetFileIconStream(string filePath, bool jumbo)
{
var shinfo = default(NativeMethods.SHFILEINFO);
var hr = NativeMethods.SHGetFileInfo(filePath, 0, ref shinfo, (uint)Marshal.SizeOf(shinfo), SHGFIICON | SHGFILARGEICON);
nint hIcon = 0;

// If requested, look up the Jumbo icon
if (jumbo)
{
hIcon = GetLargestIcon(filePath);
}

// If we didn't want the JUMBO icon, or didn't find it, fall back to
// the normal icon lookup
if (hIcon == 0)
{
var shinfo = default(NativeMethods.SHFILEINFO);

var hr = NativeMethods.SHGetFileInfo(filePath, 0, ref shinfo, (uint)Marshal.SizeOf(shinfo), SHGFI_ICON | SHGFI_SHELLICONSIZE);

if (hr == 0 || shinfo.hIcon == 0)
{
return null;
}

hIcon = shinfo.hIcon;
}

if (hr == 0 || shinfo.hIcon == 0)
if (hIcon == 0)
{
return null;
}

var stream = new InMemoryRandomAccessStream();

using var memoryStream = GetMemoryStreamFromIcon(shinfo.hIcon);
using var memoryStream = GetMemoryStreamFromIcon(hIcon); // this will DestroyIcon hIcon
using var outputStream = stream.GetOutputStreamAt(0);
using (var dataWriter = new DataWriter(outputStream))
{
Expand All @@ -95,4 +119,21 @@ private static MemoryStream GetMemoryStreamFromIcon(IntPtr hIcon)
var thumbnail = await file.GetThumbnailAsync(ThumbnailMode.PicturesView);
return thumbnail;
}

private static nint GetLargestIcon(string path)
{
var shinfo = default(NativeMethods.SHFILEINFO);
NativeMethods.SHGetFileInfo(path, 0, ref shinfo, (uint)Marshal.SizeOf(shinfo), SHGFI_ICON | SHGFI_SYSICONINDEX);

var hIcon = IntPtr.Zero;
var iID_IImageList = new Guid("46EB5926-582E-4017-9FDF-E8998DAA0950");
IntPtr imageListPtr;

if (NativeMethods.SHGetImageList(SHIL_JUMBO, ref iID_IImageList, out imageListPtr) == 0 && imageListPtr != IntPtr.Zero)
{
hIcon = NativeMethods.ImageList_GetIcon(imageListPtr, shinfo.iIcon, ILD_TRANSPARENT);
}

return hIcon;
}
}
Loading