mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-14 18:57:55 +01:00
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:
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>();
|
||||
|
||||
80
src/modules/cmdpal/Microsoft.CmdPal.UI/Events/RunEvents.cs
Normal file
80
src/modules/cmdpal/Microsoft.CmdPal.UI/Events/RunEvents.cs
Normal 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
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, () =>
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "https://aka.ms/CsWin32.schema.json",
|
||||
"allowMarshaling": false,
|
||||
"comInterop": {
|
||||
"preserveSigMethods": [ "*" ]
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user