cmdpal: Add "file" context items to the run items too (#40768)

After #39955, the "exe" items from the shell commands only ever have the
"Run{as admin, as other user}" commands. This adds the rest of the
"file" commands - copy path, open in explorer, etc.

This shuffles around some commands into the toolkit and common commands
project to make this easier.

<img width="814" height="505" alt="image"
src="https://github.com/user-attachments/assets/36ae2c75-d4d6-4762-98ec-796986f39c20"
/>
This commit is contained in:
Mike Griese
2025-07-28 20:03:49 -05:00
committed by GitHub
parent 6dc2d14e13
commit 3a0487f74a
31 changed files with 500 additions and 184 deletions

View File

@@ -10,7 +10,6 @@ using System.Xml;
using ManagedCommon;
using Microsoft.CmdPal.Ext.Apps.Commands;
using Microsoft.CmdPal.Ext.Apps.Properties;
using Microsoft.CmdPal.Ext.Apps.State;
using Microsoft.CmdPal.Ext.Apps.Utils;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -96,7 +95,7 @@ public class UWPApplication : IProgram
commands.Add(
new CommandContextItem(
new CopyPathCommand(Location))
new Commands.CopyPathCommand(Location))
{
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.C),
});

View File

@@ -5,21 +5,15 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel.Design;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.IO.Abstractions;
using System.Linq;
using System.Reflection;
using System.Security;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows.Input;
using ManagedCommon;
using Microsoft.CmdPal.Ext.Apps.Commands;
using Microsoft.CmdPal.Ext.Apps.Properties;
using Microsoft.CmdPal.Ext.Apps.State;
using Microsoft.CmdPal.Ext.Apps.Utils;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -190,7 +184,7 @@ public class Win32Program : IProgram
public List<IContextItem> GetCommands()
{
List<IContextItem> commands = new List<IContextItem>();
List<IContextItem> commands = [];
if (AppType != ApplicationType.InternetShortcutApplication && AppType != ApplicationType.Folder && AppType != ApplicationType.GenericFile)
{
@@ -208,7 +202,7 @@ public class Win32Program : IProgram
}
commands.Add(new CommandContextItem(
new CopyPathCommand(FullPath))
new Commands.CopyPathCommand(FullPath))
{
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.C),
});

View File

@@ -1,34 +0,0 @@
// 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.Ext.Indexer.Data;
using Microsoft.CmdPal.Ext.Indexer.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Indexer.Commands;
internal sealed partial class CopyPathCommand : InvokableCommand
{
private readonly IndexerItem _item;
internal CopyPathCommand(IndexerItem item)
{
this._item = item;
this.Name = Resources.Indexer_Command_CopyPath;
this.Icon = new IconInfo("\uE8c8");
}
public override CommandResult Invoke()
{
try
{
ClipboardHelper.SetText(_item.FullPath);
}
catch
{
}
return CommandResult.KeepOpen();
}
}

View File

@@ -1,43 +0,0 @@
// 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;
using System.Threading.Tasks;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.AI.Actions.Hosting;
namespace Microsoft.CmdPal.Ext.Indexer.Commands;
internal sealed partial class ExecuteActionCommand : InvokableCommand
{
private readonly ActionInstance actionInstance;
internal ExecuteActionCommand(ActionInstance actionInstance)
{
this.actionInstance = actionInstance;
this.Name = actionInstance.DisplayInfo.Description;
this.Icon = new IconInfo(actionInstance.Definition.IconFullPath);
}
public override CommandResult Invoke()
{
var task = Task.Run(InvokeAsync);
task.Wait();
return task.Result;
}
private async Task<CommandResult> InvokeAsync()
{
try
{
await actionInstance.InvokeAsync();
return CommandResult.GoHome();
}
catch (Exception ex)
{
return CommandResult.ShowToast("Failed to invoke action " + actionInstance.Definition.Id + ": " + ex.Message);
}
}
}

View File

@@ -1,44 +0,0 @@
// 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.ComponentModel;
using System.Diagnostics;
using ManagedCommon;
using Microsoft.CmdPal.Ext.Indexer.Data;
using Microsoft.CmdPal.Ext.Indexer.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Indexer.Commands;
internal sealed partial class OpenFileCommand : InvokableCommand
{
private readonly IndexerItem _item;
internal OpenFileCommand(IndexerItem item)
{
this._item = item;
this.Name = Resources.Indexer_Command_OpenFile;
this.Icon = Icons.OpenFileIcon;
}
public override CommandResult Invoke()
{
using (var process = new Process())
{
process.StartInfo.FileName = _item.FullPath;
process.StartInfo.UseShellExecute = true;
try
{
process.Start();
}
catch (Win32Exception ex)
{
Logger.LogError($"Unable to open {_item.FullPath}", ex);
}
}
return CommandResult.GoHome();
}
}

View File

@@ -1,45 +0,0 @@
// 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.ComponentModel;
using System.Diagnostics;
using System.IO;
using ManagedCommon;
using Microsoft.CmdPal.Ext.Indexer.Data;
using Microsoft.CmdPal.Ext.Indexer.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Indexer.Commands;
internal sealed partial class OpenInConsoleCommand : InvokableCommand
{
private readonly IndexerItem _item;
internal OpenInConsoleCommand(IndexerItem item)
{
this._item = item;
this.Name = Resources.Indexer_Command_OpenPathInConsole;
this.Icon = new IconInfo("\uE756");
}
public override CommandResult Invoke()
{
using (var process = new Process())
{
process.StartInfo.WorkingDirectory = Path.GetDirectoryName(_item.FullPath);
process.StartInfo.FileName = "cmd.exe";
try
{
process.Start();
}
catch (Win32Exception ex)
{
Logger.LogError($"Unable to open {_item.FullPath}", ex);
}
}
return CommandResult.GoHome();
}
}

View File

@@ -1,66 +0,0 @@
// 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;
using System.Runtime.InteropServices;
using ManagedCommon;
using ManagedCsWin32;
using Microsoft.CmdPal.Ext.Indexer.Data;
using Microsoft.CmdPal.Ext.Indexer.Indexer.Utils;
using Microsoft.CmdPal.Ext.Indexer.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Win32.UI.WindowsAndMessaging;
namespace Microsoft.CmdPal.Ext.Indexer.Commands;
internal sealed partial class OpenPropertiesCommand : InvokableCommand
{
private readonly IndexerItem _item;
private static unsafe bool ShowFileProperties(string filename)
{
var propertiesPtr = Marshal.StringToHGlobalUni("properties");
var filenamePtr = Marshal.StringToHGlobalUni(filename);
try
{
var info = new Shell32.SHELLEXECUTEINFOW
{
CbSize = (uint)sizeof(Shell32.SHELLEXECUTEINFOW),
LpVerb = propertiesPtr,
LpFile = filenamePtr,
Show = (int)SHOW_WINDOW_CMD.SW_SHOW,
FMask = NativeHelpers.SEEMASKINVOKEIDLIST,
};
return Shell32.ShellExecuteEx(ref info);
}
finally
{
Marshal.FreeHGlobal(filenamePtr);
Marshal.FreeHGlobal(propertiesPtr);
}
}
internal OpenPropertiesCommand(IndexerItem item)
{
this._item = item;
this.Name = Resources.Indexer_Command_OpenProperties;
this.Icon = new IconInfo("\uE90F");
}
public override CommandResult Invoke()
{
try
{
ShowFileProperties(_item.FullPath);
}
catch (Exception ex)
{
Logger.LogError("Error showing file properties: ", ex);
}
return CommandResult.GoHome();
}
}

View File

@@ -1,57 +0,0 @@
// 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.Runtime.InteropServices;
using ManagedCsWin32;
using Microsoft.CmdPal.Ext.Indexer.Data;
using Microsoft.CmdPal.Ext.Indexer.Indexer.Utils;
using Microsoft.CmdPal.Ext.Indexer.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Win32.UI.WindowsAndMessaging;
namespace Microsoft.CmdPal.Ext.Indexer.Commands;
internal sealed partial class OpenWithCommand : InvokableCommand
{
private readonly IndexerItem _item;
private static unsafe bool OpenWith(string filename)
{
var filenamePtr = Marshal.StringToHGlobalUni(filename);
var verbPtr = Marshal.StringToHGlobalUni("openas");
try
{
var info = new Shell32.SHELLEXECUTEINFOW
{
CbSize = (uint)sizeof(Shell32.SHELLEXECUTEINFOW),
LpVerb = verbPtr,
LpFile = filenamePtr,
Show = (int)SHOW_WINDOW_CMD.SW_SHOWNORMAL,
FMask = NativeHelpers.SEEMASKINVOKEIDLIST,
};
return Shell32.ShellExecuteEx(ref info);
}
finally
{
Marshal.FreeHGlobal(filenamePtr);
Marshal.FreeHGlobal(verbPtr);
}
}
internal OpenWithCommand(IndexerItem item)
{
this._item = item;
this.Name = Resources.Indexer_Command_OpenWith;
this.Icon = new IconInfo("\uE7AC");
}
public override CommandResult Invoke()
{
OpenWith(_item.FullPath);
return CommandResult.GoHome();
}
}

View File

@@ -12,6 +12,16 @@ internal sealed class IndexerItem
internal string FileName { get; init; }
internal IndexerItem()
{
}
internal IndexerItem(string fullPath)
{
FullPath = fullPath;
FileName = Path.GetFileName(fullPath);
}
internal bool IsDirectory()
{
if (!Path.Exists(FullPath))

View File

@@ -3,10 +3,11 @@
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using Microsoft.CmdPal.Ext.Indexer.Commands;
using System.IO;
using System.Linq;
using Microsoft.CmdPal.Common.Commands;
using Microsoft.CmdPal.Ext.Indexer.Pages;
using Microsoft.CmdPal.Ext.Indexer.Properties;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Foundation.Metadata;
@@ -28,51 +29,79 @@ internal sealed partial class IndexerListItem : ListItem
public IndexerListItem(
IndexerItem indexerItem,
IncludeBrowseCommand browseByDefault = IncludeBrowseCommand.Include)
: base(new OpenFileCommand(indexerItem))
: base()
{
FilePath = indexerItem.FullPath;
Title = indexerItem.FileName;
Subtitle = indexerItem.FullPath;
List<CommandContextItem> context = [];
if (indexerItem.IsDirectory())
var commands = FileCommands(indexerItem.FullPath, browseByDefault);
if (commands.Any())
{
var directoryPage = new DirectoryPage(indexerItem.FullPath);
Command = commands.First().Command;
MoreCommands = commands.Skip(1).ToArray();
}
}
public static IEnumerable<CommandContextItem> FileCommands(string fullPath)
{
return FileCommands(fullPath, IncludeBrowseCommand.Include);
}
internal static IEnumerable<CommandContextItem> FileCommands(
string fullPath,
IncludeBrowseCommand browseByDefault = IncludeBrowseCommand.Include)
{
List<CommandContextItem> commands = [];
if (!Path.Exists(fullPath))
{
return commands;
}
// detect whether it is a directory or file
var attr = File.GetAttributes(fullPath);
var isDir = (attr & FileAttributes.Directory) == FileAttributes.Directory;
var openCommand = new OpenFileCommand(fullPath) { Name = Resources.Indexer_Command_OpenFile };
if (isDir)
{
var directoryPage = new DirectoryPage(fullPath);
if (browseByDefault == IncludeBrowseCommand.AsDefault)
{
// Swap the open file command into the context menu
context.Add(new CommandContextItem(Command));
Command = directoryPage;
// AsDefault: browse dir first, then open in explorer
commands.Add(new CommandContextItem(directoryPage));
commands.Add(new CommandContextItem(openCommand));
}
else if (browseByDefault == IncludeBrowseCommand.Include)
{
context.Add(new CommandContextItem(directoryPage));
// AsDefault: open in explorer first, then browse
commands.Add(new CommandContextItem(openCommand));
commands.Add(new CommandContextItem(directoryPage));
}
else if (browseByDefault == IncludeBrowseCommand.Exclude)
{
// AsDefault: Just open in explorer
commands.Add(new CommandContextItem(openCommand));
}
}
IContextItem[] moreCommands = [
..context,
new CommandContextItem(new OpenWithCommand(indexerItem))];
commands.Add(new CommandContextItem(new OpenWithCommand(fullPath)));
commands.Add(new CommandContextItem(new ShowFileInFolderCommand(fullPath) { Name = Resources.Indexer_Command_ShowInFolder }));
commands.Add(new CommandContextItem(new CopyPathCommand(fullPath) { Name = Resources.Indexer_Command_CopyPath }));
commands.Add(new CommandContextItem(new OpenInConsoleCommand(fullPath)));
commands.Add(new CommandContextItem(new OpenPropertiesCommand(fullPath)));
if (IsActionsFeatureEnabled && ApiInformation.IsApiContractPresent("Windows.AI.Actions.ActionsContract", 4))
{
var actionsListContextItem = new ActionsListContextItem(indexerItem.FullPath);
var actionsListContextItem = new ActionsListContextItem(fullPath);
if (actionsListContextItem.AnyActions())
{
moreCommands = [
.. moreCommands,
actionsListContextItem
];
commands.Add(actionsListContextItem);
}
}
MoreCommands = [
.. moreCommands,
new CommandContextItem(new ShowFileInFolderCommand(indexerItem.FullPath) { Name = Resources.Indexer_Command_ShowInFolder }),
new CommandContextItem(new CopyPathCommand(indexerItem)),
new CommandContextItem(new OpenInConsoleCommand(indexerItem)),
new CommandContextItem(new OpenPropertiesCommand(indexerItem)),
];
return commands;
}
}

View File

@@ -60,7 +60,7 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, System
if (Path.Exists(query))
{
// Exit 1: The query is a direct path to a file. Great! Return it.
var item = new IndexerItem() { FullPath = query, FileName = Path.GetFileName(query) };
var item = new IndexerItem(fullPath: query);
var listItemForUs = new IndexerListItem(item, IncludeBrowseCommand.AsDefault);
Command = listItemForUs.Command;
MoreCommands = listItemForUs.MoreCommands;

View File

@@ -10,14 +10,15 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.CsWin32">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<ProjectReference Include="..\..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\..\..\common\ManagedCsWin32\ManagedCsWin32.csproj" />
<PackageReference Include="Microsoft.Windows.CsWin32">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<ProjectReference Include="..\..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\..\..\common\ManagedCsWin32\ManagedCsWin32.csproj" />
<ProjectReference Include="..\..\Microsoft.CmdPal.Common\Microsoft.CmdPal.Common.csproj" />
<ProjectReference Include="..\..\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
<ProjectReference Include="..\..\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
</ItemGroup>
<ItemGroup>

View File

@@ -3,7 +3,7 @@
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using Microsoft.CmdPal.Ext.Indexer.Commands;
using Microsoft.CmdPal.Common.Commands;
using Microsoft.CmdPal.Ext.Indexer.Data;
using Microsoft.CmdPal.Ext.Indexer.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -41,16 +41,16 @@ internal sealed partial class ExploreListItem : ListItem
}
else
{
Command = new OpenFileCommand(indexerItem);
Command = new OpenFileCommand(indexerItem.FullPath);
}
MoreCommands = [
..context,
new CommandContextItem(new OpenWithCommand(indexerItem)),
new CommandContextItem(new OpenWithCommand(indexerItem.FullPath)),
new CommandContextItem(new ShowFileInFolderCommand(indexerItem.FullPath) { Name = Resources.Indexer_Command_ShowInFolder }),
new CommandContextItem(new CopyPathCommand(indexerItem)),
new CommandContextItem(new OpenInConsoleCommand(indexerItem)),
new CommandContextItem(new OpenPropertiesCommand(indexerItem)),
new CommandContextItem(new CopyPathCommand(indexerItem.FullPath)),
new CommandContextItem(new OpenInConsoleCommand(indexerItem.FullPath)),
new CommandContextItem(new OpenPropertiesCommand(indexerItem.FullPath)),
];
}
}

View File

@@ -13,6 +13,7 @@
<None Remove="Assets\Run.svg" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Microsoft.CmdPal.Common\Microsoft.CmdPal.Common.csproj" />
<ProjectReference Include="..\..\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
<ProjectReference Include="..\..\Microsoft.CmdPal.Common\Microsoft.CmdPal.Common.csproj" />
</ItemGroup>

View File

@@ -7,6 +7,7 @@ using System.Threading.Tasks;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Storage.Streams;
using Windows.System;
namespace Microsoft.CmdPal.Ext.Shell.Pages;
@@ -54,13 +55,13 @@ internal sealed partial class RunExeItem : ListItem
{
Name = Properties.Resources.cmd_run_as_administrator,
Icon = Icons.AdminIcon,
}),
}) { RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.Enter) },
new CommandContextItem(
new AnonymousCommand(RunAsOther)
{
Name = Properties.Resources.cmd_run_as_user,
Icon = Icons.UserIcon,
}),
}) { RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.U) },
];
}

View File

@@ -26,7 +26,7 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable
private readonly IRunHistoryService _historyService;
private RunExeItem? _exeItem;
private ListItem? _exeItem;
private List<ListItem> _pathItems = [];
private ListItem? _uriItem;
@@ -319,20 +319,19 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable
.ToArray();
}
internal static RunExeItem CreateExeItem(string exe, string args, string fullExePath, Action<string>? addToHistory)
internal static ListItem CreateExeItem(string exe, string args, string fullExePath, Action<string>? addToHistory)
{
// PathToListItem will return a RunExeItem if it can find a executable.
// It will ALSO add the file search commands to the RunExeItem.
return PathToListItem(fullExePath, exe, args, addToHistory) as RunExeItem ??
new RunExeItem(exe, args, fullExePath, addToHistory);
return PathToListItem(fullExePath, exe, args, addToHistory);
}
private void CreateAndAddExeItems(string exe, string args, string fullExePath)
{
// If we already have an exe item, and the exe is the same, we can just update it
if (_exeItem != null && _exeItem.FullExePath.Equals(fullExePath, StringComparison.OrdinalIgnoreCase))
if (_exeItem is RunExeItem exeItem && exeItem.FullExePath.Equals(fullExePath, StringComparison.OrdinalIgnoreCase))
{
_exeItem.UpdateArgs(args);
exeItem.UpdateArgs(args);
}
else
{
@@ -345,7 +344,8 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable
// Is this path an executable?
// check all the extensions in PATHEXT
var extensions = Environment.GetEnvironmentVariable("PATHEXT")?.Split(';') ?? Array.Empty<string>();
return extensions.Any(ext => string.Equals(Path.GetExtension(path), ext, StringComparison.OrdinalIgnoreCase));
var extension = Path.GetExtension(path);
return string.IsNullOrEmpty(extension) || extensions.Any(ext => string.Equals(extension, ext, StringComparison.OrdinalIgnoreCase));
}
private async Task CreatePathItemsAsync(string searchPath, string originalPath, CancellationToken cancellationToken)

View File

@@ -4,8 +4,10 @@
using System;
using System.IO;
using Microsoft.CmdPal.Common.Commands;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.System;
namespace Microsoft.CmdPal.Ext.Shell;
@@ -58,18 +60,15 @@ internal sealed partial class PathListItem : ListItem
}
TextToSuggest = suggestion;
MoreCommands = [
new CommandContextItem(new CopyTextCommand(path) { Name = Properties.Resources.copy_path_command_name }) { }
];
// TODO: Follow-up during 0.4. Add the indexer commands here.
// MoreCommands = [
// new CommandContextItem(new OpenWithCommand(indexerItem)),
// new CommandContextItem(new ShowFileInFolderCommand(indexerItem.FullPath) { Name = Resources.Indexer_Command_ShowInFolder }),
// new CommandContextItem(new CopyPathCommand(indexerItem)),
// new CommandContextItem(new OpenInConsoleCommand(indexerItem)),
// new CommandContextItem(new OpenPropertiesCommand(indexerItem)),
// ];
MoreCommands = [
new CommandContextItem(new OpenWithCommand(path)),
new CommandContextItem(new ShowFileInFolderCommand(path)) { RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.E) },
new CommandContextItem(new CopyPathCommand(path) { Name = Properties.Resources.copy_path_command_name }) { RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.C) },
new CommandContextItem(new OpenInConsoleCommand(path)) { RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.R) },
new CommandContextItem(new OpenPropertiesCommand(path)),
];
_icon = new Lazy<IconInfo>(() =>
{
var iconStream = ThumbnailHelper.GetThumbnail(path).Result;