Add support for using the RunDlg history to initialize the run history

This commit is contained in:
Mike Griese
2025-06-18 08:43:48 -05:00
parent 9fc7a180d4
commit c953ce7eca
13 changed files with 262 additions and 20 deletions

View File

@@ -0,0 +1,27 @@
// 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.Collections.Generic;
namespace Microsoft.CmdPal.Common.Services;
public interface IRunHistoryService
{
/// <summary>
/// Gets the run history.
/// </summary>
/// <returns>A list of run history items.</returns>
IReadOnlyList<string> GetRunHistory();
/// <summary>
/// Clears the run history.
/// </summary>
void ClearRunHistory();
/// <summary>
/// Adds a run history item.
/// </summary>
/// <param name="item">The run history item to add.</param>
void AddRunHistoryItem(string item);
}

View File

@@ -23,6 +23,8 @@ public partial class AppStateModel : ObservableObject
// STATE HERE
public RecentCommandsManager RecentCommands { get; private set; } = new();
public List<string> RunHistory { get; private set; } = [];
// END SETTINGS
///////////////////////////////////////////////////////////////////////////
@@ -86,7 +88,7 @@ public partial class AppStateModel : ObservableObject
{
foreach (var item in newSettings)
{
savedSettings[item.Key] = item.Value != null ? item.Value.DeepClone() : null;
savedSettings[item.Key] = item.Value?.DeepClone();
}
var serialized = savedSettings.ToJsonString(JsonSerializationContext.Default.AppStateModel.Options);
@@ -121,20 +123,4 @@ public partial class AppStateModel : ObservableObject
// now, the settings is just next to the exe
return Path.Combine(directory, "state.json");
}
// [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "<Pending>")]
// private static readonly JsonSerializerOptions _serializerOptions = new()
// {
// WriteIndented = true,
// Converters = { new JsonStringEnumConverter() },
// };
// private static readonly JsonSerializerOptions _deserializerOptions = new()
// {
// PropertyNameCaseInsensitive = true,
// IncludeFields = true,
// AllowTrailingCommas = true,
// PreferredObjectCreationHandling = JsonObjectCreationHandling.Populate,
// ReadCommentHandling = JsonCommentHandling.Skip,
// };
}

View File

@@ -99,6 +99,9 @@ public partial class App : Application
var files = new IndexerCommandsProvider();
files.SuppressFallbackWhen(ShellCommandsProvider.SuppressFileFallbackIf);
services.AddSingleton<ICommandProvider>(allApps);
// var run = new ShellCommandsProvider();
// var hist = Microsoft.Terminal.UI.IconPathConverter.CreateRunHistory();
services.AddSingleton<ICommandProvider, ShellCommandsProvider>();
services.AddSingleton<ICommandProvider, CalculatorCommandProvider>();
services.AddSingleton<ICommandProvider>(files);
@@ -144,6 +147,7 @@ public partial class App : Application
services.AddSingleton(state);
services.AddSingleton<IExtensionService, ExtensionService>();
services.AddSingleton<TrayIconService>();
services.AddSingleton<IRunHistoryService, RunHistoryService>();
// ViewModels
services.AddSingleton<ShellViewModel>();

View File

@@ -0,0 +1,42 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Common.Services;
using Microsoft.CmdPal.UI.ViewModels;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace Microsoft.CmdPal.UI;
internal sealed class RunHistoryService : IRunHistoryService
{
private readonly AppStateModel _appStateModel;
public RunHistoryService(AppStateModel appStateModel)
{
_appStateModel = appStateModel;
}
public IReadOnlyList<string> GetRunHistory()
{
if (_appStateModel.RunHistory.Count == 0)
{
var history = Microsoft.Terminal.UI.IconPathConverter.CreateRunHistory();
_appStateModel.RunHistory.AddRange(history);
}
return _appStateModel.RunHistory;
}
public void ClearRunHistory()
{
_appStateModel.RunHistory.Clear();
}
public void AddRunHistoryItem(string item)
{
_appStateModel.RunHistory.Add(item);
AppStateModel.SaveState(_appStateModel);
}
}

View File

@@ -383,4 +383,83 @@ namespace winrt::Microsoft::Terminal::UI::implementation
icon.Height(targetSize);
return icon;
}
///////
// Run history
// Largely copied from the Run work circa 2022.
winrt::Windows::Foundation::Collections::IVector<hstring> IconPathConverter::CreateRunHistory()
{
// Load MRU history
std::vector<hstring> history;
wil::unique_hmodule _comctl;
HANDLE(WINAPI* _createMRUList)(MRUINFO* lpmi);
int(WINAPI* _enumMRUList)(HANDLE hMRU,int nItem,void* lpData,UINT uLen);
void(WINAPI *_freeMRUList)(HANDLE hMRU);
int(WINAPI *_addMRUString)(HANDLE hMRU, LPCWSTR szString);
// Lazy load comctl32.dll
// Theoretically, we could cache this into a magic static, but we shouldn't need to actually do this more than once in CmdPal
_comctl.reset(LoadLibraryExW(L"comctl32.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32));
_createMRUList = reinterpret_cast<decltype(_createMRUList)>(GetProcAddress(_comctl.get(), "CreateMRUListW"));
FAIL_FAST_LAST_ERROR_IF(!_createMRUList);
_enumMRUList = reinterpret_cast<decltype(_enumMRUList)>(GetProcAddress(_comctl.get(), "EnumMRUListW"));
FAIL_FAST_LAST_ERROR_IF(!_enumMRUList);
_freeMRUList = reinterpret_cast<decltype(_freeMRUList)>(GetProcAddress(_comctl.get(), "FreeMRUList"));
FAIL_FAST_LAST_ERROR_IF(!_freeMRUList);
_addMRUString = reinterpret_cast<decltype(_addMRUString)>(GetProcAddress(_comctl.get(), "AddMRUStringW"));
FAIL_FAST_LAST_ERROR_IF(!_addMRUString);
static const WCHAR c_szRunMRU[] = REGSTR_PATH_EXPLORER L"\\RunMRU";
MRUINFO mi = {
sizeof(mi),
26,
MRU_CACHEWRITE,
HKEY_CURRENT_USER,
c_szRunMRU,
NULL // NOTE: use default string compare
// since this is a GLOBAL MRU
};
if (const auto hmru = _createMRUList(&mi))
{
auto freeMRUList = wil::scope_exit([=]() {
_freeMRUList(hmru);
});
for (int nMax = _enumMRUList(hmru, -1, NULL, 0), i = 0; i < nMax; ++i)
{
WCHAR szCommand[MAX_PATH + 2];
const auto length = _enumMRUList(hmru, i, szCommand, ARRAYSIZE(szCommand));
if (length > 1)
{
// clip off the null-terminator
std::wstring_view text{ szCommand, wil::safe_cast<size_t>(length - 1) };
//#pragma disable warning(C26493)
#pragma warning( push )
#pragma warning( disable : 26493 )
if (text.back() == L'\\')
{
// old MRU format has a slash at the end with the show cmd
text = { szCommand, wil::safe_cast<size_t>(length - 2) };
#pragma warning( pop )
if (text.empty())
{
continue;
}
}
history.emplace_back(text);
}
}
}
// Update dropdown & initial value
return winrt::single_threaded_observable_vector<winrt::hstring>(std::move(history));
}
}

View File

@@ -1,6 +1,7 @@
#pragma once
#include "IconPathConverter.g.h"
#include "types.h"
namespace winrt::Microsoft::Terminal::UI::implementation
{
@@ -13,6 +14,12 @@ namespace winrt::Microsoft::Terminal::UI::implementation
static Microsoft::UI::Xaml::Controls::IconSource IconSourceMUX(const winrt::hstring& iconPath, bool convertToGrayscale, const int targetSize=24);
static Microsoft::UI::Xaml::Controls::IconElement IconMUX(const winrt::hstring& iconPath);
static Microsoft::UI::Xaml::Controls::IconElement IconMUX(const winrt::hstring& iconPath, const int targetSize);
static winrt::Windows::Foundation::Collections::IVector<hstring> CreateRunHistory();
private:
winrt::Windows::Foundation::Collections::IVector<hstring> _mruHistory;
};
}

View File

@@ -10,6 +10,8 @@ namespace Microsoft.Terminal.UI
static Microsoft.UI.Xaml.Controls.IconSource IconSourceMUX(String path, Boolean convertToGrayscale);
static Microsoft.UI.Xaml.Controls.IconElement IconMUX(String path);
static Microsoft.UI.Xaml.Controls.IconElement IconMUX(String path, Int32 targetSize);
static Windows.Foundation.Collections.IVector<String> CreateRunHistory();
};
}

View File

@@ -64,6 +64,8 @@
// WIL
#include <wil/com.h>
#include <wil/resource.h>
#include <wil/safecast.h>
#include <wil/stl.h>
#include <wil/filesystem.h>
// Due to the use of RESOURCE_SUPPRESS_STL in result.h, we need to include resource.h first, which happens
@@ -90,6 +92,7 @@
#include <winrt/Windows.ApplicationModel.Resources.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Graphics.Imaging.h>
#include <Windows.Graphics.Imaging.Interop.h>

View File

@@ -0,0 +1,21 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#define MRU_CACHEWRITE 0x0002
#define REGSTR_PATH_EXPLORER TEXT("Software\\Microsoft\\Windows\\CurrentVersion\\Explorer")
// https://learn.microsoft.com/en-us/windows/win32/shell/mrucmpproc
typedef int(CALLBACK* MRUCMPPROC)(
LPCTSTR pString1,
LPCTSTR pString2);
// https://learn.microsoft.com/en-us/windows/win32/shell/mruinfo
struct MRUINFO
{
DWORD cbSize;
UINT uMax;
UINT fFlags;
HKEY hKey;
LPCTSTR lpszSubKey;
MRUCMPPROC lpfnCompare;
};

View File

@@ -4,11 +4,13 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.CmdPal.Ext.Shell.Commands;
using Microsoft.CmdPal.Ext.Shell.Pages;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Shell.Helpers;
@@ -159,4 +161,53 @@ public class ShellListPageHelpers
}
}
}
internal static ListItem? ListItemForCommandString(string query)
{
var li = new ListItem();
var searchText = query.Trim();
var expanded = Environment.ExpandEnvironmentVariables(searchText);
searchText = expanded;
if (string.IsNullOrEmpty(searchText) || string.IsNullOrWhiteSpace(searchText))
{
return null;
}
ShellListPage.ParseExecutableAndArgs(searchText, out var exe, out var args);
var exeExists = ShellListPageHelpers.FileExistInPath(exe, out var fullExePath);
var pathIsDir = Directory.Exists(exe);
Debug.WriteLine($"Run: exeExists={exeExists}, pathIsDir={pathIsDir}");
if (exeExists)
{
// TODO we need to probably get rid of the settings for this provider entirely
var exeItem = ShellListPage.CreateExeItems(exe, args, fullExePath);
li.Command = exeItem.Command;
li.Title = exeItem.Title;
li.Subtitle = exeItem.Subtitle;
li.Icon = exeItem.Icon;
li.MoreCommands = exeItem.MoreCommands;
}
else if (pathIsDir)
{
var pathItem = new PathListItem(exe, query);
li.Command = pathItem.Command;
li.Title = pathItem.Title;
li.Subtitle = pathItem.Subtitle;
li.Icon = pathItem.Icon;
li.MoreCommands = pathItem.MoreCommands;
}
else if (System.Uri.TryCreate(searchText, UriKind.Absolute, out var uri))
{
li.Command = new OpenUrlCommand(searchText) { Result = CommandResult.Dismiss() };
li.Title = searchText;
}
else
{
return null;
}
return li;
}
}

View File

@@ -14,6 +14,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
<ProjectReference Include="..\..\Microsoft.CmdPal.Common\Microsoft.CmdPal.Common.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.CommandLine" />

View File

@@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using Microsoft.CmdPal.Common.Services;
using Microsoft.CmdPal.Ext.Shell.Helpers;
using Microsoft.CmdPal.Ext.Shell.Properties;
using Microsoft.CommandPalette.Extensions;
@@ -21,16 +22,18 @@ internal sealed partial class ShellListPage : DynamicListPage
private readonly List<ListItem> _exeItems = [];
private readonly List<ListItem> _topLevelItems = [];
private readonly List<ListItem> _historyItems = [];
private readonly IRunHistoryService _historyService;
private List<ListItem> _pathItems = [];
private ListItem? _uriItem;
public ShellListPage(SettingsManager settingsManager, bool addBuiltins = false)
public ShellListPage(SettingsManager settingsManager, IRunHistoryService runHistoryService, bool addBuiltins = false)
{
Icon = Icons.RunV2;
Id = "com.microsoft.cmdpal.shell";
Name = Resources.cmd_plugin_name;
PlaceholderText = Resources.list_placeholder_text;
_helper = new(settingsManager);
_historyService = runHistoryService;
EmptyContent = new CommandItem()
{
@@ -66,6 +69,15 @@ internal sealed partial class ShellListPage : DynamicListPage
// i.Icon = Icons.RunV2;
// i.Subtitle = string.Empty;
// });
var hist = _historyService.GetRunHistory();
var filteredHist = string.IsNullOrEmpty(searchText) ?
hist :
ListHelpers.FilterList(hist, searchText, (q, s) => StringMatcher.FuzzySearch(q, s).Score);
var histItems = filteredHist
.Select(h => ShellListPageHelpers.ListItemForCommandString(h))
.Where(i => i != null)
.Select(i => i!);
ListHelpers.InPlaceUpdateList(_historyItems, histItems);
// TODO we can be smarter about only re-reading the filesystem if the
// new search is just the oldSearch+some chars

View File

@@ -2,6 +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 Microsoft.CmdPal.Common.Services;
using Microsoft.CmdPal.Ext.Shell.Helpers;
using Microsoft.CmdPal.Ext.Shell.Pages;
using Microsoft.CmdPal.Ext.Shell.Properties;
@@ -15,10 +16,14 @@ public partial class ShellCommandsProvider : CommandProvider
private readonly CommandItem _shellPageItem;
private readonly SettingsManager _settingsManager = new();
private readonly ShellListPage _shellListPage;
private readonly FallbackCommandItem _fallbackItem;
private readonly IRunHistoryService _historyService;
public ShellCommandsProvider()
public ShellCommandsProvider(IRunHistoryService runHistoryService)
{
_historyService = runHistoryService;
Id = "Run";
DisplayName = Resources.cmd_plugin_name;
Icon = Icons.RunV2;
@@ -26,7 +31,9 @@ public partial class ShellCommandsProvider : CommandProvider
_fallbackItem = new FallbackExecuteItem(_settingsManager);
_shellPageItem = new CommandItem(new ShellListPage(_settingsManager))
_shellListPage = new ShellListPage(_settingsManager, _historyService);
_shellPageItem = new CommandItem(_shellListPage)
{
Icon = Icons.RunV2,
Title = Resources.shell_command_name,