CmdPal: collection of Run Commands nits (#42092)

* Path items were being treated inconsistently
* We shouldn't re-enumerate a directory on every keystroke
* A bunch of elements had empty TextToSuggest (which makes it crazier
  that it ever worked right)


Vaguely regressed in #41956
related to #39091
This commit is contained in:
Mike Griese
2025-10-02 06:36:59 -05:00
committed by GitHub
parent 55f0bcc441
commit 87af08630a
22 changed files with 418 additions and 452 deletions

View File

@@ -44,6 +44,9 @@ foreach ($csprojFile in $csprojFilesArray) {
if ($csprojFile -like '*Microsoft.CmdPal.Core.*.csproj') {
continue
}
if ($csprojFile -like '*Microsoft.CmdPal.Ext.Shell.csproj') {
continue
}
$importExists = Test-ImportSharedCsWinRTProps -filePath $csprojFile
if (!$importExists) {

View File

@@ -2,8 +2,6 @@
// 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;
namespace Microsoft.CmdPal.Core.Common.Services;
public interface IRunHistoryService
@@ -25,3 +23,12 @@ public interface IRunHistoryService
/// <param name="item">The run history item to add.</param>
void AddRunHistoryItem(string item);
}
public interface ITelemetryService
{
void LogRunQuery(string query, int resultCount, ulong durationMs);
void LogRunCommand(string command, bool asAdmin, bool success);
void LogOpenUri(string uri, bool isWeb, bool success);
}

View File

@@ -160,7 +160,7 @@ public partial class App : Application
services.AddSingleton<IRootPageService, PowerToysRootPageService>();
services.AddSingleton<IAppHostService, PowerToysAppHostService>();
services.AddSingleton(new TelemetryForwarder());
services.AddSingleton<ITelemetryService, TelemetryForwarder>();
// ViewModels
services.AddSingleton<ShellViewModel>();

View File

@@ -0,0 +1,80 @@
// 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.Diagnostics.CodeAnalysis;
using System.Diagnostics.Tracing;
using Microsoft.PowerToys.Telemetry;
using Microsoft.PowerToys.Telemetry.Events;
namespace Microsoft.CmdPal.UI.Events;
// Just put all the run events in one file for simplicity.
#pragma warning disable SA1402 // File may only contain a single type
#pragma warning disable SA1649 // File name should match first type name
[EventData]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
public class CmdPalRunQuery : EventBase, IEvent
{
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
public string Query { get; set; }
public int ResultCount { get; set; }
public ulong DurationMs { get; set; }
public CmdPalRunQuery(string query, int resultCount, ulong durationMs)
{
EventName = "CmdPal_RunQuery";
Query = query;
ResultCount = resultCount;
DurationMs = durationMs;
}
}
[EventData]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
public class CmdPalRunCommand : EventBase, IEvent
{
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
public string Command { get; set; }
public bool AsAdmin { get; set; }
public bool Success { get; set; }
public CmdPalRunCommand(string command, bool asAdmin, bool success)
{
EventName = "CmdPal_RunCommand";
Command = command;
AsAdmin = asAdmin;
Success = success;
}
}
[EventData]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
public class CmdPalOpenUri : EventBase, IEvent
{
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
public string Uri { get; set; }
public bool IsWeb { get; set; }
public bool Success { get; set; }
public CmdPalOpenUri(string uri, bool isWeb, bool success)
{
EventName = "CmdPal_OpenUri";
Uri = uri;
IsWeb = isWeb;
Success = success;
}
}
#pragma warning restore SA1649 // File name should match first type name
#pragma warning restore SA1402 // File may only contain a single type

View File

@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.Core.Common.Services;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CmdPal.UI.Events;
using Microsoft.PowerToys.Telemetry;
@@ -19,6 +20,7 @@ namespace Microsoft.CmdPal.UI;
/// or something similar, but this works for now.
/// </summary>
internal sealed class TelemetryForwarder :
ITelemetryService,
IRecipient<BeginInvokeMessage>,
IRecipient<CmdPalInvokeResultMessage>
{
@@ -37,4 +39,19 @@ internal sealed class TelemetryForwarder :
{
PowerToysTelemetry.Log.WriteEvent(new BeginInvoke());
}
public void LogRunQuery(string query, int resultCount, ulong durationMs)
{
PowerToysTelemetry.Log.WriteEvent(new CmdPalRunQuery(query, resultCount, durationMs));
}
public void LogRunCommand(string command, bool asAdmin, bool success)
{
PowerToysTelemetry.Log.WriteEvent(new CmdPalRunCommand(command, asAdmin, success));
}
public void LogOpenUri(string uri, bool isWeb, bool success)
{
PowerToysTelemetry.Log.WriteEvent(new CmdPalOpenUri(uri, isWeb, success));
}
}

View File

@@ -2,7 +2,6 @@
// 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.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
@@ -83,7 +82,7 @@ public class QueryTests : CommandPaletteUnitTestBase
var settings = Settings.CreateDefaultSettings();
var mockHistory = CreateMockHistoryService();
var pages = new ShellListPage(settings, mockHistory.Object);
var pages = new ShellListPage(settings, mockHistory.Object, telemetryService: null);
await UpdatePageAndWaitForItems(pages, () =>
{
@@ -115,7 +114,7 @@ public class QueryTests : CommandPaletteUnitTestBase
var settings = Settings.CreateDefaultSettings();
var mockHistoryService = CreateMockHistoryServiceWithCommonCommands();
var pages = new ShellListPage(settings, mockHistoryService.Object);
var pages = new ShellListPage(settings, mockHistoryService.Object, telemetryService: null);
await UpdatePageAndWaitForItems(pages, () =>
{
@@ -141,7 +140,7 @@ public class QueryTests : CommandPaletteUnitTestBase
var settings = Settings.CreateDefaultSettings();
var mockHistoryService = CreateMockHistoryServiceWithCommonCommands();
var pages = new ShellListPage(settings, mockHistoryService.Object);
var pages = new ShellListPage(settings, mockHistoryService.Object, telemetryService: null);
await UpdatePageAndWaitForItems(pages, () =>
{

View File

@@ -16,7 +16,7 @@ public class ShellCommandProviderTests
{
// Setup
var mockHistoryService = new Mock<IRunHistoryService>();
var provider = new ShellCommandsProvider(mockHistoryService.Object);
var provider = new ShellCommandsProvider(mockHistoryService.Object, telemetryService: null);
// Assert
Assert.IsNotNull(provider.DisplayName);
@@ -28,7 +28,7 @@ public class ShellCommandProviderTests
{
// Setup
var mockHistoryService = new Mock<IRunHistoryService>();
var provider = new ShellCommandsProvider(mockHistoryService.Object);
var provider = new ShellCommandsProvider(mockHistoryService.Object, telemetryService: null);
// Assert
Assert.IsNotNull(provider.Icon);
@@ -39,7 +39,7 @@ public class ShellCommandProviderTests
{
// Setup
var mockHistoryService = new Mock<IRunHistoryService>();
var provider = new ShellCommandsProvider(mockHistoryService.Object);
var provider = new ShellCommandsProvider(mockHistoryService.Object, telemetryService: null);
// Act
var commands = provider.TopLevelCommands();

View File

@@ -1,228 +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.ComponentModel;
using System.Diagnostics;
using System.IO;
using Microsoft.CmdPal.Ext.Shell.Helpers;
using Microsoft.CmdPal.Ext.Shell.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Shell.Commands;
internal sealed partial class ExecuteItem : InvokableCommand
{
private readonly ISettingsInterface _settings;
private readonly RunAsType _runas;
public string Cmd { get; internal set; } = string.Empty;
private static readonly char[] Separator = [' '];
public ExecuteItem(string cmd, ISettingsInterface settings, RunAsType type = RunAsType.None)
{
if (type == RunAsType.Administrator)
{
Name = Properties.Resources.cmd_run_as_administrator;
Icon = Icons.AdminIcon;
}
else if (type == RunAsType.OtherUser)
{
Name = Properties.Resources.cmd_run_as_user;
Icon = Icons.UserIcon;
}
else
{
Name = Properties.Resources.generic_run_command;
Icon = Icons.RunV2Icon;
}
Cmd = cmd;
_settings = settings;
_runas = type;
}
private void Execute(Func<ProcessStartInfo, Process?> startProcess, ProcessStartInfo info)
{
if (startProcess is null)
{
return;
}
try
{
startProcess(info);
}
catch (FileNotFoundException e)
{
var name = "Plugin: " + Properties.Resources.cmd_plugin_name;
var message = $"{Properties.Resources.cmd_command_not_found}: {e.Message}";
// GH TODO #138 -- show this message once that's wired up
// _context.API.ShowMsg(name, message);
}
catch (Win32Exception e)
{
var name = "Plugin: " + Properties.Resources.cmd_plugin_name;
var message = $"{Properties.Resources.cmd_command_failed}: {e.Message}";
ExtensionHost.LogMessage(new LogMessage() { Message = name + message });
// GH TODO #138 -- show this message once that's wired up
// _context.API.ShowMsg(name, message);
}
}
public static ProcessStartInfo SetProcessStartInfo(string fileName, string workingDirectory = "", string arguments = "", string verb = "")
{
var info = new ProcessStartInfo
{
FileName = fileName,
WorkingDirectory = workingDirectory,
Arguments = arguments,
Verb = verb,
};
return info;
}
private ProcessStartInfo PrepareProcessStartInfo(string command, RunAsType runAs = RunAsType.None)
{
command = Environment.ExpandEnvironmentVariables(command);
var workingDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
// Set runAsArg
var runAsVerbArg = string.Empty;
if (runAs == RunAsType.OtherUser)
{
runAsVerbArg = "runAsUser";
}
else if (runAs == RunAsType.Administrator || _settings.RunAsAdministrator)
{
runAsVerbArg = "runAs";
}
if (Enum.TryParse<ExecutionShell>(_settings.ShellCommandExecution, out var executionShell))
{
ProcessStartInfo info;
if (executionShell == ExecutionShell.Cmd)
{
var arguments = _settings.LeaveShellOpen ? $"/k \"{command}\"" : $"/c \"{command}\" & pause";
info = SetProcessStartInfo("cmd.exe", workingDirectory, arguments, runAsVerbArg);
}
else if (executionShell == ExecutionShell.Powershell)
{
var arguments = _settings.LeaveShellOpen
? $"-NoExit \"{command}\""
: $"\"{command} ; Read-Host -Prompt \\\"{Resources.run_plugin_cmd_wait_message}\\\"\"";
info = SetProcessStartInfo("powershell.exe", workingDirectory, arguments, runAsVerbArg);
}
else if (executionShell == ExecutionShell.PowerShellSeven)
{
var arguments = _settings.LeaveShellOpen
? $"-NoExit -C \"{command}\""
: $"-C \"{command} ; Read-Host -Prompt \\\"{Resources.run_plugin_cmd_wait_message}\\\"\"";
info = SetProcessStartInfo("pwsh.exe", workingDirectory, arguments, runAsVerbArg);
}
else if (executionShell == ExecutionShell.WindowsTerminalCmd)
{
var arguments = _settings.LeaveShellOpen ? $"cmd.exe /k \"{command}\"" : $"cmd.exe /c \"{command}\" & pause";
info = SetProcessStartInfo("wt.exe", workingDirectory, arguments, runAsVerbArg);
}
else if (executionShell == ExecutionShell.WindowsTerminalPowerShell)
{
var arguments = _settings.LeaveShellOpen ? $"powershell -NoExit -C \"{command}\"" : $"powershell -C \"{command}\"";
info = SetProcessStartInfo("wt.exe", workingDirectory, arguments, runAsVerbArg);
}
else if (executionShell == ExecutionShell.WindowsTerminalPowerShellSeven)
{
var arguments = _settings.LeaveShellOpen ? $"pwsh.exe -NoExit -C \"{command}\"" : $"pwsh.exe -C \"{command}\"";
info = SetProcessStartInfo("wt.exe", workingDirectory, arguments, runAsVerbArg);
}
else if (executionShell == ExecutionShell.RunCommand)
{
// Open explorer if the path is a file or directory
if (Directory.Exists(command) || File.Exists(command))
{
info = SetProcessStartInfo("explorer.exe", arguments: command, verb: runAsVerbArg);
}
else
{
var parts = command.Split(Separator, 2);
if (parts.Length == 2)
{
var filename = parts[0];
if (ShellListPageHelpers.FileExistInPath(filename))
{
var arguments = parts[1];
if (_settings.LeaveShellOpen)
{
// Wrap the command in a cmd.exe process
info = SetProcessStartInfo("cmd.exe", workingDirectory, $"/k \"{filename} {arguments}\"", runAsVerbArg);
}
else
{
info = SetProcessStartInfo(filename, workingDirectory, arguments, runAsVerbArg);
}
}
else
{
if (_settings.LeaveShellOpen)
{
// Wrap the command in a cmd.exe process
info = SetProcessStartInfo("cmd.exe", workingDirectory, $"/k \"{command}\"", runAsVerbArg);
}
else
{
info = SetProcessStartInfo(command, verb: runAsVerbArg);
}
}
}
else
{
if (_settings.LeaveShellOpen)
{
// Wrap the command in a cmd.exe process
info = SetProcessStartInfo("cmd.exe", workingDirectory, $"/k \"{command}\"", runAsVerbArg);
}
else
{
info = SetProcessStartInfo(command, verb: runAsVerbArg);
}
}
}
}
else
{
throw new NotImplementedException();
}
info.UseShellExecute = true;
_settings.AddCmdHistory(command);
return info;
}
else
{
ExtensionHost.LogMessage(new LogMessage() { Message = "Error extracting setting" });
throw new NotImplementedException();
}
}
public override CommandResult Invoke()
{
try
{
Execute(Process.Start, PrepareProcessStartInfo(Cmd, _runas));
}
catch
{
ExtensionHost.LogMessage(new LogMessage() { Message = "Error starting the process " });
}
return CommandResult.Dismiss();
}
}

View File

@@ -2,9 +2,7 @@
// 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.IO;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CmdPal.Core.Common.Services;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Shell;
@@ -13,18 +11,33 @@ internal sealed partial class OpenUrlWithHistoryCommand : OpenUrlCommand
{
private readonly Action<string>? _addToHistory;
private readonly string _url;
private readonly ITelemetryService? _telemetryService;
public OpenUrlWithHistoryCommand(string url, Action<string>? addToHistory = null)
public OpenUrlWithHistoryCommand(string url, Action<string>? addToHistory = null, ITelemetryService? telemetryService = null)
: base(url)
{
_addToHistory = addToHistory;
_url = url;
_telemetryService = telemetryService;
}
public override CommandResult Invoke()
{
_addToHistory?.Invoke(_url);
var result = base.Invoke();
return result;
var success = ShellHelpers.OpenInShell(_url);
var isWebUrl = false;
if (Uri.TryCreate(_url, UriKind.Absolute, out var uri))
{
if (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps)
{
isWebUrl = true;
}
}
_telemetryService?.LogOpenUri(_url, isWebUrl, success);
return CommandResult.Dismiss();
}
}

View File

@@ -2,14 +2,9 @@
// 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.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CmdPal.Core.Common.Services;
using Microsoft.CmdPal.Ext.Shell.Helpers;
using Microsoft.CmdPal.Ext.Shell.Pages;
using Microsoft.CmdPal.Ext.Shell.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Shell;
@@ -19,18 +14,20 @@ internal sealed partial class FallbackExecuteItem : FallbackCommandItem, IDispos
private static readonly char[] _systemDirectoryRoots = ['\\', '/'];
private readonly Action<string>? _addToHistory;
private readonly ITelemetryService _telemetryService;
private CancellationTokenSource? _cancellationTokenSource;
private Task? _currentUpdateTask;
public FallbackExecuteItem(SettingsManager settings, Action<string>? addToHistory)
public FallbackExecuteItem(SettingsManager settings, Action<string>? addToHistory, ITelemetryService telemetryService)
: base(
new NoOpCommand() { Id = "com.microsoft.run.fallback" },
Resources.shell_command_display_title)
ResourceLoaderInstance.GetString("shell_command_display_title"))
{
Title = string.Empty;
Subtitle = Properties.Resources.generic_run_command;
Subtitle = ResourceLoaderInstance.GetString("generic_run_command");
Icon = Icons.RunV2Icon; // Defined in Icons.cs and contains the execute command icon.
_addToHistory = addToHistory;
_telemetryService = telemetryService;
}
public override void UpdateQuery(string query)
@@ -147,7 +144,7 @@ internal sealed partial class FallbackExecuteItem : FallbackCommandItem, IDispos
if (exeExists)
{
// TODO we need to probably get rid of the settings for this provider entirely
var exeItem = ShellListPage.CreateExeItem(exe, args, fullExePath, _addToHistory);
var exeItem = ShellListPage.CreateExeItem(exe, args, fullExePath, _addToHistory, telemetryService: _telemetryService);
Title = exeItem.Title;
Subtitle = exeItem.Subtitle;
Icon = exeItem.Icon;
@@ -156,7 +153,7 @@ internal sealed partial class FallbackExecuteItem : FallbackCommandItem, IDispos
}
else if (pathIsDir)
{
var pathItem = new PathListItem(exe, query, _addToHistory);
var pathItem = new PathListItem(exe, query, _addToHistory, _telemetryService);
Command = pathItem.Command;
MoreCommands = pathItem.MoreCommands;
Title = pathItem.Title;
@@ -165,7 +162,7 @@ internal sealed partial class FallbackExecuteItem : FallbackCommandItem, IDispos
}
else if (System.Uri.TryCreate(searchText, UriKind.Absolute, out var uri))
{
Command = new OpenUrlWithHistoryCommand(searchText, _addToHistory) { Result = CommandResult.Dismiss() };
Command = new OpenUrlWithHistoryCommand(searchText, _addToHistory, _telemetryService) { Result = CommandResult.Dismiss() };
Title = searchText;
}
else

View File

@@ -2,10 +2,10 @@
// 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.IO;
using System.Runtime.InteropServices;
using System.Text;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Storage.FileSystem;
namespace Microsoft.CmdPal.Ext.Shell.Helpers;
@@ -19,38 +19,11 @@ namespace Microsoft.CmdPal.Ext.Shell.Helpers;
public static class CommandLineNormalizer
{
#pragma warning disable SA1310 // Field names should not contain underscore
private const int MAX_PATH = 260;
private const uint INVALID_FILE_ATTRIBUTES = 0xFFFFFFFF;
private const uint FILE_ATTRIBUTE_DIRECTORY = 0x10;
private const int MAX_PATH = 260;
#pragma warning restore SA1310 // Field names should not contain underscore
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern uint ExpandEnvironmentStringsW(
[MarshalAs(UnmanagedType.LPWStr)] string lpSrc,
[MarshalAs(UnmanagedType.LPWStr)] StringBuilder lpDst,
uint nSize);
[DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern IntPtr CommandLineToArgvW(
[MarshalAs(UnmanagedType.LPWStr)] string lpCmdLine,
out int pNumArgs);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern uint SearchPathW(
[MarshalAs(UnmanagedType.LPWStr)] string? lpPath,
[MarshalAs(UnmanagedType.LPWStr)] string lpFileName,
[MarshalAs(UnmanagedType.LPWStr)] string? lpExtension,
uint nBufferLength,
[MarshalAs(UnmanagedType.LPWStr)] StringBuilder lpBuffer,
out IntPtr lpFilePart);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern uint GetFileAttributesW(
[MarshalAs(UnmanagedType.LPWStr)] string lpFileName);
[DllImport("kernel32.dll")]
private static extern IntPtr LocalFree(IntPtr hMem);
/// <summary>
/// Normalizes a command line string by expanding environment variables, resolving executable paths,
/// and standardizing the format for comparison purposes.
@@ -129,9 +102,9 @@ public static class CommandLineNormalizer
private static string ExpandEnvironmentVariables(string input)
{
const int initialBufferSize = 1024;
var buffer = new StringBuilder(initialBufferSize);
var buffer = new char[initialBufferSize];
var result = ExpandEnvironmentStringsW(input, buffer, (uint)buffer.Capacity);
var result = PInvoke.ExpandEnvironmentStrings(input, buffer);
if (result == 0)
{
@@ -139,11 +112,11 @@ public static class CommandLineNormalizer
return input;
}
if (result > buffer.Capacity)
if (result > buffer.Length)
{
// Buffer was too small, resize and try again
buffer.Capacity = (int)result;
result = ExpandEnvironmentStringsW(input, buffer, (uint)buffer.Capacity);
buffer = new char[result];
result = PInvoke.ExpandEnvironmentStrings(input, buffer);
if (result == 0)
{
@@ -151,7 +124,7 @@ public static class CommandLineNormalizer
}
}
return buffer.ToString();
return new string(buffer, 0, (int)result - 1); // -1 to exclude null terminator
}
/// <summary>
@@ -159,28 +132,30 @@ public static class CommandLineNormalizer
/// </summary>
private static string[] ParseCommandLineToArguments(string commandLine)
{
var argv = CommandLineToArgvW(commandLine, out var argc);
if (argv == IntPtr.Zero || argc == 0)
unsafe
{
return Array.Empty<string>();
}
var argv = PInvoke.CommandLineToArgv(commandLine, out var argc);
try
{
var args = new string[argc];
for (var i = 0; i < argc; i++)
if (argv == null || argc == 0)
{
var argPtr = Marshal.ReadIntPtr(argv, i * IntPtr.Size);
args[i] = Marshal.PtrToStringUni(argPtr) ?? string.Empty;
return Array.Empty<string>();
}
return args;
}
finally
{
LocalFree(argv);
try
{
var args = new string[argc];
for (var i = 0; i < argc; i++)
{
args[i] = new string(argv[i]);
}
return args;
}
finally
{
PInvoke.LocalFree(new HLOCAL(argv));
}
}
}
@@ -227,39 +202,46 @@ public static class CommandLineNormalizer
/// </summary>
private static string TryResolveExecutable(string executableName)
{
var buffer = new StringBuilder(MAX_PATH);
var buffer = new char[MAX_PATH];
var result = SearchPathW(
null, // Use default search path
executableName,
".exe", // Default extension
(uint)buffer.Capacity,
buffer,
out var _); // We don't need the file part
if (result == 0)
unsafe
{
return string.Empty;
}
var outParam = default(PWSTR); // ultimately discarded
if (result > buffer.Capacity)
{
// Buffer was too small, resize and try again
buffer.Capacity = (int)result;
result = SearchPathW(null, executableName, ".exe", (uint)buffer.Capacity, buffer, out var _);
var result = PInvoke.SearchPath(
null, // Use default search path
executableName,
".exe", // Default extension
buffer,
&outParam); // We don't need the file part
if (result == 0)
{
return string.Empty;
}
if (result > buffer.Length)
{
// Buffer was too small, resize and try again
buffer = new char[result];
result = PInvoke.SearchPath(null, executableName, ".exe", buffer, &outParam);
if (result == 0)
{
return string.Empty;
}
}
var resolvedPath = new string(buffer, 0, (int)result);
// Verify the resolved path exists and is not a directory
var attributes = PInvoke.GetFileAttributes(resolvedPath);
return attributes == INVALID_FILE_ATTRIBUTES ||
(attributes & (uint)FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_DIRECTORY) != 0 ?
string.Empty :
resolvedPath;
}
var resolvedPath = buffer.ToString();
// Verify the resolved path exists and is not a directory
var attributes = GetFileAttributesW(resolvedPath);
return attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0 ? string.Empty : resolvedPath;
}
/// <summary>

View File

@@ -2,13 +2,8 @@
// 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.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CmdPal.Ext.Shell.Commands;
using Microsoft.CmdPal.Core.Common.Services;
using Microsoft.CmdPal.Ext.Shell.Pages;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -16,37 +11,6 @@ namespace Microsoft.CmdPal.Ext.Shell.Helpers;
public class ShellListPageHelpers
{
private static readonly CompositeFormat CmdHasBeenExecutedTimes = System.Text.CompositeFormat.Parse(Properties.Resources.cmd_has_been_executed_times);
private readonly ISettingsInterface _settings;
public ShellListPageHelpers(ISettingsInterface settings)
{
_settings = settings;
}
private ListItem GetCurrentCmd(string cmd)
{
var result = new ListItem(new ExecuteItem(cmd, _settings))
{
Title = cmd,
Subtitle = Properties.Resources.cmd_plugin_name + ": " + Properties.Resources.cmd_execute_through_shell,
Icon = new IconInfo(string.Empty),
};
return result;
}
public List<CommandContextItem> LoadContextMenus(ListItem listItem)
{
var resultList = new List<CommandContextItem>
{
new(new ExecuteItem(listItem.Title, _settings, RunAsType.Administrator)),
new(new ExecuteItem(listItem.Title, _settings, RunAsType.OtherUser )),
};
return resultList;
}
internal static bool FileExistInPath(string filename)
{
return FileExistInPath(filename, out var _);
@@ -58,7 +22,7 @@ public class ShellListPageHelpers
return ShellHelpers.FileExistInPath(filename, out fullPath, token ?? CancellationToken.None);
}
internal static ListItem? ListItemForCommandString(string query, Action<string>? addToHistory)
internal static ListItem? ListItemForCommandString(string query, Action<string>? addToHistory, ITelemetryService? telemetryService)
{
var li = new ListItem();
@@ -100,7 +64,7 @@ public class ShellListPageHelpers
if (exeExists)
{
// TODO we need to probably get rid of the settings for this provider entirely
var exeItem = ShellListPage.CreateExeItem(exe, args, fullExePath, addToHistory);
var exeItem = ShellListPage.CreateExeItem(exe, args, fullExePath, addToHistory, telemetryService);
li.Command = exeItem.Command;
li.Title = exeItem.Title;
li.Subtitle = exeItem.Subtitle;
@@ -109,7 +73,7 @@ public class ShellListPageHelpers
}
else if (pathIsDir)
{
var pathItem = new PathListItem(exe, query, addToHistory);
var pathItem = new PathListItem(exe, query, addToHistory, telemetryService);
li.Command = pathItem.Command;
li.Title = pathItem.Title;
li.Subtitle = pathItem.Subtitle;
@@ -118,7 +82,7 @@ public class ShellListPageHelpers
}
else if (System.Uri.TryCreate(searchText, UriKind.Absolute, out var uri))
{
li.Command = new OpenUrlWithHistoryCommand(searchText) { Result = CommandResult.Dismiss() };
li.Command = new OpenUrlWithHistoryCommand(searchText, addToHistory, telemetryService) { Result = CommandResult.Dismiss() };
li.Title = searchText;
}
else

View File

@@ -1,11 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\..\Common.Dotnet.AotCompatibility.props" />
<Import Project="..\Common.ExtDependencies.props" />
<Import Project="..\..\CoreCommonProps.props" />
<PropertyGroup>
<Nullable>enable</Nullable>
<RootNamespace>Microsoft.CmdPal.Ext.Shell</RootNamespace>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
@@ -16,7 +12,6 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Core\Microsoft.CmdPal.Core.Common\Microsoft.CmdPal.Core.Common.csproj" />
<ProjectReference Include="..\..\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.CommandLine" />

View File

@@ -0,0 +1,7 @@
{
"$schema": "https://aka.ms/CsWin32.schema.json",
"allowMarshaling": false,
"comInterop": {
"preserveSigMethods": [ "*" ]
}
}

View File

@@ -0,0 +1,22 @@
GetCurrentPackageFullName
SetWindowLong
GetWindowLong
WINDOW_EX_STYLE
SFBS_FLAGS
MAX_PATH
GetDpiForWindow
GetWindowRect
GetMonitorInfo
SetWindowPos
MonitorFromWindow
SHOW_WINDOW_CMD
ShellExecuteEx
SEE_MASK_INVOKEIDLIST
ExpandEnvironmentStringsW
CommandLineToArgvW
SearchPathW
GetFileAttributesW
LocalFree
FILE_FLAGS_AND_ATTRIBUTES

View File

@@ -2,9 +2,8 @@
// 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.IO;
using Microsoft.CmdPal.Core.Common.Commands;
using Microsoft.CmdPal.Core.Common.Services;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.System;
@@ -13,13 +12,18 @@ namespace Microsoft.CmdPal.Ext.Shell;
internal sealed partial class PathListItem : ListItem
{
private readonly Lazy<IconInfo> _icon;
private readonly bool _isDirectory;
private readonly Lazy<bool> fetchedIcon;
private readonly bool isDirectory;
private readonly string path;
public override IIconInfo? Icon { get => _icon.Value; set => base.Icon = value; }
public override IIconInfo? Icon { get => fetchedIcon.Value ? _icon : _icon; set => base.Icon = value; }
public PathListItem(string path, string originalDir, Action<string>? addToHistory)
: base(new OpenUrlWithHistoryCommand(path, addToHistory))
private IIconInfo? _icon;
internal bool IsDirectory => isDirectory;
public PathListItem(string path, string originalDir, Action<string>? addToHistory, ITelemetryService? telemetryService = null)
: base(new OpenUrlWithHistoryCommand(path, addToHistory, telemetryService))
{
var fileName = Path.GetFileName(path);
if (string.IsNullOrEmpty(fileName))
@@ -27,8 +31,8 @@ internal sealed partial class PathListItem : ListItem
fileName = Path.GetFileName(Path.GetDirectoryName(path)) ?? string.Empty;
}
_isDirectory = Directory.Exists(path);
if (_isDirectory)
isDirectory = Directory.Exists(path);
if (isDirectory)
{
if (!path.EndsWith('\\'))
{
@@ -41,6 +45,8 @@ internal sealed partial class PathListItem : ListItem
}
}
this.path = path;
Title = fileName; // Just the name of the file is the Title
Subtitle = path; // What the user typed is the subtitle
@@ -58,23 +64,35 @@ internal sealed partial class PathListItem : ListItem
// wrap it in quotes
suggestion = string.Concat("\"", suggestion, "\"");
}
else
{
suggestion = path;
}
TextToSuggest = suggestion;
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 CopyPathCommand(path)) { 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>(() =>
fetchedIcon = new Lazy<bool>(() =>
{
var iconStream = ThumbnailHelper.GetThumbnail(path).Result;
var icon = iconStream is not null ? IconInfo.FromStream(iconStream) :
_isDirectory ? Icons.FolderIcon : Icons.RunV2Icon;
return icon;
_ = Task.Run(FetchIconAsync);
return true;
});
}
private async Task FetchIconAsync()
{
var iconStream = await ThumbnailHelper.GetThumbnail(path);
var icon = iconStream != null ?
IconInfo.FromStream(iconStream) :
isDirectory ? Icons.FolderIcon : Icons.RunV2Icon;
_icon = icon;
OnPropertyChanged(nameof(Icon));
}
}

View File

@@ -2,8 +2,8 @@
// 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.CmdPal.Core.Common.Services;
using Microsoft.CmdPal.Ext.Shell.Helpers;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Storage.Streams;
@@ -15,6 +15,7 @@ internal sealed partial class RunExeItem : ListItem
{
private readonly Lazy<IconInfo> _icon;
private readonly Action<string>? _addToHistory;
private readonly ITelemetryService? _telemetryService;
public override IIconInfo? Icon { get => _icon.Value; set => base.Icon = value; }
@@ -26,13 +27,18 @@ internal sealed partial class RunExeItem : ListItem
private string FullString => string.IsNullOrEmpty(_args) ? Exe : $"{Exe} {_args}";
public RunExeItem(string exe, string args, string fullExePath, Action<string>? addToHistory)
public RunExeItem(
string exe,
string args,
string fullExePath,
Action<string>? addToHistory,
ITelemetryService? telemetryService = null)
{
FullExePath = fullExePath;
Exe = exe;
var command = new AnonymousCommand(Run)
{
Name = Properties.Resources.generic_run_command,
Name = ResourceLoaderInstance.GetString("generic_run_command"),
Result = CommandResult.Dismiss(),
};
Command = command;
@@ -46,6 +52,7 @@ internal sealed partial class RunExeItem : ListItem
});
_addToHistory = addToHistory;
_telemetryService = telemetryService;
UpdateArgs(args);
@@ -53,13 +60,13 @@ internal sealed partial class RunExeItem : ListItem
new CommandContextItem(
new AnonymousCommand(RunAsAdmin)
{
Name = Properties.Resources.cmd_run_as_administrator,
Name = ResourceLoaderInstance.GetString("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,
Name = ResourceLoaderInstance.GetString("cmd_run_as_user"),
Icon = Icons.UserIcon,
}) { RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.U) },
];
@@ -97,20 +104,26 @@ internal sealed partial class RunExeItem : ListItem
{
_addToHistory?.Invoke(FullString);
ShellHelpers.OpenInShell(FullExePath, _args);
var success = ShellHelpers.OpenInShell(FullExePath, _args);
_telemetryService?.LogRunCommand(FullString, false, success);
}
public void RunAsAdmin()
{
_addToHistory?.Invoke(FullString);
ShellHelpers.OpenInShell(FullExePath, _args, runAs: ShellHelpers.ShellRunAsType.Administrator);
var success = ShellHelpers.OpenInShell(FullExePath, _args, runAs: ShellHelpers.ShellRunAsType.Administrator);
_telemetryService?.LogRunCommand(FullString, true, success);
}
public void RunAsOther()
{
_addToHistory?.Invoke(FullString);
ShellHelpers.OpenInShell(FullExePath, _args, runAs: ShellHelpers.ShellRunAsType.OtherUser);
var success = ShellHelpers.OpenInShell(FullExePath, _args, runAs: ShellHelpers.ShellRunAsType.OtherUser);
_telemetryService?.LogRunCommand(FullString, false, success);
}
}

View File

@@ -2,15 +2,8 @@
// 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.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CmdPal.Core.Common.Services;
using Microsoft.CmdPal.Ext.Shell.Helpers;
using Microsoft.CmdPal.Ext.Shell.Properties;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -18,13 +11,13 @@ namespace Microsoft.CmdPal.Ext.Shell.Pages;
internal sealed partial class ShellListPage : DynamicListPage, IDisposable
{
private readonly ShellListPageHelpers _helper;
private readonly List<ListItem> _topLevelItems = [];
private readonly Dictionary<string, ListItem> _historyItems = [];
private readonly List<ListItem> _currentHistoryItems = [];
private readonly IRunHistoryService _historyService;
private readonly ITelemetryService? _telemetryService;
private readonly Dictionary<string, ListItem> _currentPathItems = new();
private ListItem? _exeItem;
private List<ListItem> _pathItems = [];
@@ -35,27 +28,26 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable
private bool _loadedInitialHistory;
public ShellListPage(ISettingsInterface settingsManager, IRunHistoryService runHistoryService, bool addBuiltins = false)
private string _currentSubdir = string.Empty;
public ShellListPage(
ISettingsInterface settingsManager,
IRunHistoryService runHistoryService,
ITelemetryService? telemetryService)
{
Icon = Icons.RunV2Icon;
Id = "com.microsoft.cmdpal.shell";
Name = Resources.cmd_plugin_name;
PlaceholderText = Resources.list_placeholder_text;
_helper = new(settingsManager);
Name = ResourceLoaderInstance.GetString("cmd_plugin_name");
PlaceholderText = ResourceLoaderInstance.GetString("list_placeholder_text");
_historyService = runHistoryService;
_telemetryService = telemetryService;
EmptyContent = new CommandItem()
{
Title = Resources.cmd_plugin_name,
Title = ResourceLoaderInstance.GetString("cmd_plugin_name"),
Icon = Icons.RunV2Icon,
Subtitle = Resources.list_placeholder_text,
Subtitle = ResourceLoaderInstance.GetString("list_placeholder_text"),
};
if (addBuiltins)
{
// here, we _could_ add built-in providers if we wanted. links to apps, calc, etc.
// That would be a truly run-first experience
}
}
public override void UpdateSearchText(string oldSearch, string newSearch)
@@ -123,8 +115,13 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable
private async Task BuildListItemsForSearchAsync(string newSearch, CancellationToken cancellationToken)
{
var timer = System.Diagnostics.Stopwatch.StartNew();
// Check for cancellation at the start
cancellationToken.ThrowIfCancellationRequested();
if (cancellationToken.IsCancellationRequested)
{
return;
}
// If the search text is the start of a path to a file (it might be a
// UNC path), then we want to list all the files that start with that text:
@@ -136,7 +133,10 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable
var expanded = Environment.ExpandEnvironmentVariables(searchText);
// Check for cancellation after environment expansion
cancellationToken.ThrowIfCancellationRequested();
if (cancellationToken.IsCancellationRequested)
{
return;
}
// TODO we can be smarter about only re-reading the filesystem if the
// new search is just the oldSearch+some chars
@@ -206,7 +206,10 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable
couldResolvePath = false;
}
cancellationToken.ThrowIfCancellationRequested();
if (cancellationToken.IsCancellationRequested)
{
return;
}
_pathItems.Clear();
@@ -221,7 +224,10 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable
}
// Check for cancellation before creating exe items
cancellationToken.ThrowIfCancellationRequested();
if (cancellationToken.IsCancellationRequested)
{
return;
}
if (couldResolvePath && exeExists)
{
@@ -278,17 +284,31 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable
_currentHistoryItems.AddRange(filteredHistory);
// Final cancellation check
cancellationToken.ThrowIfCancellationRequested();
if (cancellationToken.IsCancellationRequested)
{
return;
}
timer.Stop();
_telemetryService?.LogRunQuery(newSearch, GetItems().Length, (ulong)timer.ElapsedMilliseconds);
}
private static ListItem PathToListItem(string path, string originalPath, string args = "", Action<string>? addToHistory = null)
private static ListItem PathToListItem(string path, string originalPath, string args = "", Action<string>? addToHistory = null, ITelemetryService? telemetryService = null)
{
var pathItem = new PathListItem(path, originalPath, addToHistory);
var pathItem = new PathListItem(path, originalPath, addToHistory, telemetryService);
if (pathItem.IsDirectory)
{
return pathItem;
}
// Is this path an executable? If so, then make a RunExeItem
if (IsExecutable(path))
{
var exeItem = new RunExeItem(Path.GetFileName(path), args, path, addToHistory);
var exeItem = new RunExeItem(Path.GetFileName(path), args, path, addToHistory, telemetryService)
{
TextToSuggest = path,
};
exeItem.MoreCommands = [
.. exeItem.MoreCommands,
@@ -306,24 +326,22 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable
LoadInitialHistory();
}
var filteredTopLevel = ListHelpers.FilterList(_topLevelItems, SearchText);
List<ListItem> uriItems = _uriItem is not null ? [_uriItem] : [];
List<ListItem> exeItems = _exeItem is not null ? [_exeItem] : [];
return
exeItems
.Concat(filteredTopLevel)
.Concat(_currentHistoryItems)
.Concat(_pathItems)
.Concat(uriItems)
.ToArray();
}
internal static ListItem CreateExeItem(string exe, string args, string fullExePath, Action<string>? addToHistory)
internal static ListItem CreateExeItem(string exe, string args, string fullExePath, Action<string>? addToHistory, ITelemetryService? telemetryService)
{
// 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);
return PathToListItem(fullExePath, exe, args, addToHistory, telemetryService);
}
private void CreateAndAddExeItems(string exe, string args, string fullExePath)
@@ -335,7 +353,7 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable
}
else
{
_exeItem = CreateExeItem(exe, args, fullExePath, AddToHistory);
_exeItem = CreateExeItem(exe, args, fullExePath, AddToHistory, _telemetryService);
}
}
@@ -389,7 +407,10 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable
}
// Check for cancellation before directory operations
cancellationToken.ThrowIfCancellationRequested();
if (cancellationToken.IsCancellationRequested)
{
return;
}
var dirExists = Directory.Exists(directoryPath);
@@ -408,30 +429,71 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable
if (dirExists)
{
// Check for cancellation before file system enumeration
cancellationToken.ThrowIfCancellationRequested();
if (cancellationToken.IsCancellationRequested)
{
return;
}
if (directoryPath == _currentSubdir)
{
// Filter the items we already had
var fuzzyString = searchPattern.TrimEnd('*');
var newMatchedPathItems = new List<ListItem>();
foreach (var kv in _currentPathItems)
{
var score = string.IsNullOrEmpty(fuzzyString) ?
1 :
FuzzyStringMatcher.ScoreFuzzy(fuzzyString, kv.Key);
if (score > 0)
{
newMatchedPathItems.Add(kv.Value);
}
}
ListHelpers.InPlaceUpdateList(_pathItems, newMatchedPathItems);
return;
}
// Get all the files in the directory that start with the search text
// Run this on a background thread to avoid blocking
var files = await Task.Run(() => Directory.GetFileSystemEntries(directoryPath, searchPattern), cancellationToken);
// Check for cancellation after file enumeration
cancellationToken.ThrowIfCancellationRequested();
if (cancellationToken.IsCancellationRequested)
{
return;
}
var searchPathTrailer = trimmed.Remove(0, Math.Min(directoryPath.Length, trimmed.Length));
var originalBeginning = originalPath.Remove(originalPath.Length - searchPathTrailer.Length);
var originalBeginning = originalPath.EndsWith(searchPathTrailer, StringComparison.CurrentCultureIgnoreCase) ?
originalPath.Remove(originalPath.Length - searchPathTrailer.Length) :
originalPath;
if (isDriveRoot)
{
originalBeginning = string.Concat(originalBeginning, '\\');
}
// Create a list of commands for each file
var commands = files.Select(f => PathToListItem(f, originalBeginning)).ToList();
var newPathItems = files
.Select(f => PathToListItem(f, originalBeginning))
.ToDictionary(item => item.Title, item => item);
// Final cancellation check before updating results
cancellationToken.ThrowIfCancellationRequested();
if (cancellationToken.IsCancellationRequested)
{
return;
}
// Add the commands to the list
_pathItems = commands;
_pathItems = newPathItems.Values.ToList();
_currentSubdir = directoryPath;
_currentPathItems.Clear();
foreach ((var k, IListItem v) in newPathItems)
{
_currentPathItems[k] = (ListItem)v;
}
}
else
{
@@ -458,7 +520,7 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable
{
var hist = _historyService.GetRunHistory();
var histItems = hist
.Select(h => (h, ShellListPageHelpers.ListItemForCommandString(h, AddToHistory)))
.Select(h => (h, ShellListPageHelpers.ListItemForCommandString(h, AddToHistory, _telemetryService)))
.Where(tuple => tuple.Item2 is not null)
.Select(tuple => (tuple.h, tuple.Item2!))
.ToList();

View File

@@ -0,0 +1,13 @@
// 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.Ext.Shell;
internal static class ResourceLoaderInstance
{
public static string GetString(string resourceKey)
{
return Properties.Resources.ResourceManager.GetString(resourceKey, Properties.Resources.Culture) ?? throw new InvalidOperationException($"Resource key '{resourceKey}' not found.");
}
}

View File

@@ -19,19 +19,21 @@ public partial class ShellCommandsProvider : CommandProvider
private readonly ShellListPage _shellListPage;
private readonly FallbackCommandItem _fallbackItem;
private readonly IRunHistoryService _historyService;
private readonly ITelemetryService _telemetryService;
public ShellCommandsProvider(IRunHistoryService runHistoryService)
public ShellCommandsProvider(IRunHistoryService runHistoryService, ITelemetryService telemetryService)
{
_historyService = runHistoryService;
_telemetryService = telemetryService;
Id = "com.microsoft.cmdpal.builtin.run";
DisplayName = Resources.cmd_plugin_name;
Icon = Icons.RunV2Icon;
Settings = _settingsManager.Settings;
_shellListPage = new ShellListPage(_settingsManager, _historyService);
_shellListPage = new ShellListPage(_settingsManager, _historyService, _telemetryService);
_fallbackItem = new FallbackExecuteItem(_settingsManager, _shellListPage.AddToHistory);
_fallbackItem = new FallbackExecuteItem(_settingsManager, _shellListPage.AddToHistory, _telemetryService);
_shellPageItem = new CommandItem(_shellListPage)
{