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,