From c953ce7ecaecceda0a1f49519972877978841102 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Wed, 18 Jun 2025 08:43:48 -0500 Subject: [PATCH] Add support for using the RunDlg history to initialize the run history --- .../Services/IRunHistoryService.cs | 27 +++++++ .../AppStateModel.cs | 20 +---- .../cmdpal/Microsoft.CmdPal.UI/App.xaml.cs | 4 + .../RunHistoryService.xaml.cs | 42 ++++++++++ .../IconPathConverter.cpp | 79 +++++++++++++++++++ .../Microsoft.Terminal.UI/IconPathConverter.h | 7 ++ .../IconPathConverter.idl | 2 + .../cmdpal/Microsoft.Terminal.UI/pch.h | 3 + .../cmdpal/Microsoft.Terminal.UI/types.h | 21 +++++ .../Helpers/ShellListPageHelpers.cs | 51 ++++++++++++ .../Microsoft.CmdPal.Ext.Shell.csproj | 1 + .../Pages/ShellListPage.cs | 14 +++- .../ShellCommandsProvider.cs | 11 ++- 13 files changed, 262 insertions(+), 20 deletions(-) create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.Common/Services/IRunHistoryService.cs create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI/RunHistoryService.xaml.cs create mode 100644 src/modules/cmdpal/Microsoft.Terminal.UI/types.h diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Services/IRunHistoryService.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Services/IRunHistoryService.cs new file mode 100644 index 0000000000..703ad9f1ff --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Services/IRunHistoryService.cs @@ -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 +{ + /// + /// Gets the run history. + /// + /// A list of run history items. + IReadOnlyList GetRunHistory(); + + /// + /// Clears the run history. + /// + void ClearRunHistory(); + + /// + /// Adds a run history item. + /// + /// The run history item to add. + void AddRunHistoryItem(string item); +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AppStateModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AppStateModel.cs index 649e49fbc7..00764b8c82 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AppStateModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AppStateModel.cs @@ -23,6 +23,8 @@ public partial class AppStateModel : ObservableObject // STATE HERE public RecentCommandsManager RecentCommands { get; private set; } = new(); + public List 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 = "")] - // 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, - // }; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs index 761e91678a..b2d5569736 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs @@ -99,6 +99,9 @@ public partial class App : Application var files = new IndexerCommandsProvider(); files.SuppressFallbackWhen(ShellCommandsProvider.SuppressFileFallbackIf); services.AddSingleton(allApps); + + // var run = new ShellCommandsProvider(); + // var hist = Microsoft.Terminal.UI.IconPathConverter.CreateRunHistory(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(files); @@ -144,6 +147,7 @@ public partial class App : Application services.AddSingleton(state); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); // ViewModels services.AddSingleton(); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/RunHistoryService.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/RunHistoryService.xaml.cs new file mode 100644 index 0000000000..889efcb75d --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/RunHistoryService.xaml.cs @@ -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 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); + } +} diff --git a/src/modules/cmdpal/Microsoft.Terminal.UI/IconPathConverter.cpp b/src/modules/cmdpal/Microsoft.Terminal.UI/IconPathConverter.cpp index f35394f0fe..ef9d5b7382 100644 --- a/src/modules/cmdpal/Microsoft.Terminal.UI/IconPathConverter.cpp +++ b/src/modules/cmdpal/Microsoft.Terminal.UI/IconPathConverter.cpp @@ -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 IconPathConverter::CreateRunHistory() + { + // Load MRU history + std::vector 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(GetProcAddress(_comctl.get(), "CreateMRUListW")); + FAIL_FAST_LAST_ERROR_IF(!_createMRUList); + + _enumMRUList = reinterpret_cast(GetProcAddress(_comctl.get(), "EnumMRUListW")); + FAIL_FAST_LAST_ERROR_IF(!_enumMRUList); + + _freeMRUList = reinterpret_cast(GetProcAddress(_comctl.get(), "FreeMRUList")); + FAIL_FAST_LAST_ERROR_IF(!_freeMRUList); + + _addMRUString = reinterpret_cast(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(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(length - 2) }; +#pragma warning( pop ) + if (text.empty()) + { + continue; + } + } + history.emplace_back(text); + } + } + } + + // Update dropdown & initial value + return winrt::single_threaded_observable_vector(std::move(history)); + } } diff --git a/src/modules/cmdpal/Microsoft.Terminal.UI/IconPathConverter.h b/src/modules/cmdpal/Microsoft.Terminal.UI/IconPathConverter.h index 8c637ef371..326b50cf2f 100644 --- a/src/modules/cmdpal/Microsoft.Terminal.UI/IconPathConverter.h +++ b/src/modules/cmdpal/Microsoft.Terminal.UI/IconPathConverter.h @@ -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 CreateRunHistory(); + + private: + winrt::Windows::Foundation::Collections::IVector _mruHistory; }; } diff --git a/src/modules/cmdpal/Microsoft.Terminal.UI/IconPathConverter.idl b/src/modules/cmdpal/Microsoft.Terminal.UI/IconPathConverter.idl index 5b6f677003..a4b093ac50 100644 --- a/src/modules/cmdpal/Microsoft.Terminal.UI/IconPathConverter.idl +++ b/src/modules/cmdpal/Microsoft.Terminal.UI/IconPathConverter.idl @@ -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 CreateRunHistory(); }; } diff --git a/src/modules/cmdpal/Microsoft.Terminal.UI/pch.h b/src/modules/cmdpal/Microsoft.Terminal.UI/pch.h index f87ee3dbdd..9647d69fcc 100644 --- a/src/modules/cmdpal/Microsoft.Terminal.UI/pch.h +++ b/src/modules/cmdpal/Microsoft.Terminal.UI/pch.h @@ -64,6 +64,8 @@ // WIL #include +#include +#include #include #include // 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 #include +#include #include #include diff --git a/src/modules/cmdpal/Microsoft.Terminal.UI/types.h b/src/modules/cmdpal/Microsoft.Terminal.UI/types.h new file mode 100644 index 0000000000..e6efb2ae32 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.Terminal.UI/types.h @@ -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; +}; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Helpers/ShellListPageHelpers.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Helpers/ShellListPageHelpers.cs index 19cfd2b690..a1d9a30a67 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Helpers/ShellListPageHelpers.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Helpers/ShellListPageHelpers.cs @@ -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; + } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Microsoft.CmdPal.Ext.Shell.csproj b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Microsoft.CmdPal.Ext.Shell.csproj index bafa6e97d2..c1792064f9 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Microsoft.CmdPal.Ext.Shell.csproj +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Microsoft.CmdPal.Ext.Shell.csproj @@ -14,6 +14,7 @@ + diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Pages/ShellListPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Pages/ShellListPage.cs index 65b1226836..afc3545f64 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Pages/ShellListPage.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Pages/ShellListPage.cs @@ -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 _exeItems = []; private readonly List _topLevelItems = []; private readonly List _historyItems = []; + private readonly IRunHistoryService _historyService; private List _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 diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/ShellCommandsProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/ShellCommandsProvider.cs index 267cf9a390..b9cc08a23a 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/ShellCommandsProvider.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/ShellCommandsProvider.cs @@ -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,