Compare commits

...

81 Commits

Author SHA1 Message Date
Mike Griese
0562677379 Revert "CmdPal: A different approach to bookmarking scripts, exes" 2025-07-22 14:21:38 -05:00
Mike Griese
367504537d suggestions too 2025-07-22 10:28:00 -05:00
Mike Griese
0cfb321b9b Merge branch 'dev/migrie/f/run-page-2-with-suggestions' into dev/migrie/f/run-page-2-with-history 2025-07-22 06:56:45 -05:00
Mike Griese
4b0534acf8 Merge remote-tracking branch 'origin/main' into dev/migrie/f/run-page-2-with-suggestions 2025-07-22 06:56:29 -05:00
Mike Griese
bf83bbd94d Merge branch 'dev/migrie/f/run-page-2-with-suggestions' into dev/migrie/f/run-page-2-with-history 2025-07-16 06:55:50 -05:00
Mike Griese
12b7555006 dead code 2025-07-16 06:54:00 -05:00
Mike Griese
77a0d77471 Merge remote-tracking branch 'origin/main' into dev/migrie/f/run-page-2-with-suggestions 2025-07-16 06:53:30 -05:00
Mike Griese
45145cf1a4 Merge branch 'dev/migrie/f/run-page-2-with-suggestions' into dev/migrie/f/run-page-2-with-history 2025-07-15 13:15:04 -05:00
Mike Griese
f758907850 Merge remote-tracking branch 'origin/main' into dev/migrie/f/run-page-2-with-suggestions 2025-07-15 13:14:40 -05:00
Mike Griese
9323598fed don't add to the end of the list
honestly, kinda surprised that the dict maintains insert order?
2025-07-15 13:08:32 -05:00
Mike Griese
e8c63a26e0 okay, fine 2025-07-15 10:17:49 -05:00
Mike Griese
6f867d178c spel 2025-07-15 10:09:57 -05:00
Mike Griese
4250bd82ac Merge branch 'dev/migrie/f/run-page-2-with-suggestions' into dev/migrie/f/run-page-2-with-history 2025-07-15 10:06:52 -05:00
Mike Griese
c4047a8ee2 Merge remote-tracking branch 'origin/main' into dev/migrie/f/run-page-2-with-suggestions 2025-07-15 10:05:08 -05:00
Mike Griese
210febe270 Merge branch 'dev/migrie/f/run-page-2-with-suggestions' into dev/migrie/f/run-page-2-with-history 2025-07-09 14:56:22 -05:00
Mike Griese
e53cb409b4 Merge remote-tracking branch 'origin/main' into dev/migrie/f/run-page-2-with-suggestions 2025-07-09 14:56:07 -05:00
Mike Griese
5dee7c9d89 Merge branch 'dev/migrie/f/run-page-2-with-suggestions' into dev/migrie/f/run-page-2-with-history 2025-07-09 14:55:02 -05:00
Mike Griese
c2ba9af144 Merge remote-tracking branch 'origin/main' into dev/migrie/f/run-page-2-with-suggestions 2025-07-09 14:47:13 -05:00
Mike Griese
558e2af6cb uhg 2025-07-06 14:14:33 -05:00
Mike Griese
986ffbeede Merge branch 'dev/migrie/f/run-page-2-with-suggestions' into dev/migrie/f/run-page-2-with-history 2025-07-06 14:07:16 -05:00
Mike Griese
8466c47059 delete all this logging we don't need 2025-07-06 10:36:14 -05:00
Mike Griese
229d3b4991 loc, dead code removal 2025-07-06 09:50:49 -05:00
Mike Griese
7ca9798df5 Merge remote-tracking branch 'origin/main' into dev/migrie/f/run-page-2-with-suggestions 2025-07-06 09:24:35 -05:00
Mike Griese
a6e39d5535 PARENT: omg 2025-07-06 09:24:20 -05:00
Mike Griese
edf02497af tiny note 2025-07-06 09:24:06 -05:00
Mike Griese
02b583267d move it all out 2025-07-06 09:16:04 -05:00
Mike Griese
e2569ec4ee Start pulling this out of IconPathConverter 2025-07-06 09:07:56 -05:00
Mike Griese
c760962573 minor nits 2025-07-05 22:55:53 -05:00
Mike Griese
fb49f6a5e5 you should be with your friends 2025-07-05 05:47:25 -05:00
Mike Griese
b4a7bb4a7a MAIN: this is dumb 2025-07-04 06:58:50 -05:00
Mike Griese
8615c48c5c this works better 2025-07-03 20:16:03 -05:00
Mike Griese
1aa78e1b96 PARENT: omg 2025-07-03 15:49:42 -05:00
Mike Griese
b0c862dd67 add items to history 2025-07-03 15:27:50 -05:00
Mike Griese
b6e3b8a3ee rename 2025-07-03 12:46:29 -05:00
Mike Griese
a2d0d3b262 start adding callbacks to commands 2025-07-03 12:46:10 -05:00
Mike Griese
ee53a6d138 Use a dict for string->listitem for history 2025-07-02 06:57:02 -05:00
Mike Griese
126a3c0de8 load history items with a timeout 2025-07-02 06:20:07 -05:00
Mike Griese
a94bd91dba Merge branch 'dev/migrie/f/run-page-2-with-suggestions' into dev/migrie/f/run-page-2-with-history 2025-07-01 16:32:14 -05:00
Mike Griese
b2f2462ad6 The fallback command should also be cancellable 2025-07-01 16:13:28 -05:00
Mike Griese
b6f0ced53e Exes that are found in a dir should be RunExeItems 2025-07-01 14:46:58 -05:00
Mike Griese
a405f27d19 reuse existing exe item when we can 2025-07-01 14:30:19 -05:00
Mike Griese
86d04cc3bd timeout resolving network paths 2025-07-01 13:57:14 -05:00
Mike Griese
381482e9a0 Revert "try to async the File.Exists, but get sad"
This reverts commit 9e7d212c31.
2025-07-01 13:34:08 -05:00
Mike Griese
9e7d212c31 try to async the File.Exists, but get sad 2025-07-01 13:34:04 -05:00
Mike Griese
11c9d913cc Make run searches async, so subsequent ones cancel the previous.
thanks copilot, this actually worked
2025-07-01 12:36:01 -05:00
Mike Griese
31f5af7e14 URI only if the file doesn't exist 2025-07-01 12:17:38 -05:00
Mike Griese
568c2ca388 Merge remote-tracking branch 'origin/main' into dev/migrie/f/run-page-2-with-suggestions 2025-07-01 10:04:59 -05:00
Mike Griese
27124972cd Merge remote-tracking branch 'origin/main' into dev/migrie/f/run-page-2-with-suggestions 2025-06-30 08:39:19 -05:00
Mike Griese
ca9488c875 Merge remote-tracking branch 'origin/main' into dev/migrie/f/run-page-2-with-suggestions 2025-06-30 05:53:59 -05:00
Mike Griese
d73a8a0a2c Load the run history immediately 2025-06-18 09:37:00 -05:00
Mike Griese
c953ce7eca Add support for using the RunDlg history to initialize the run history 2025-06-18 08:43:48 -05:00
Mike Griese
9fc7a180d4 xamlformat(?) 2025-06-10 06:20:09 -05:00
Mike Griese
ccca007562 Merge remote-tracking branch 'origin/main' into dev/migrie/f/run-page-2-with-suggestions 2025-06-10 06:15:59 -05:00
Mike Griese
07c85065f2 Add support for URIs too 2025-06-09 20:42:35 -05:00
Mike Griese
c06767b73e Merge remote-tracking branch 'origin/main' into dev/migrie/f/run-page-2-with-suggestions 2025-06-08 12:46:12 -05:00
Mike Griese
4c639a085c Merge remote-tracking branch 'origin/main' into dev/migrie/f/run-page-2-with-suggestions 2025-06-08 06:32:20 -05:00
Mike Griese
8aa7349f32 handle backspacing suggestions cleaner 2025-06-08 05:56:26 -05:00
Mike Griese
fa86f2223c PARENT: deal with drives and quotes better 2025-06-08 05:56:03 -05:00
Mike Griese
30215e8b4f PARENT: much better env var handling 2025-06-06 17:58:29 -05:00
Mike Griese
ee386e7be4 PARENT: Suppress file fallback when run finds something 2025-06-05 04:52:25 -05:00
Mike Griese
9ba2030b5e OWN BRANCH: Fix the weighting for the web search fallbacks
Closes #39419
This can be moved to its own branch.
2025-06-04 06:34:26 -05:00
Mike Griese
1ba832b732 Make the calc command play better with TextToSuggest 2025-06-02 14:11:05 -05:00
Mike Griese
46f27c1612 make it build 2025-05-30 07:00:29 -05:00
Mike Griese
1d81ef8935 PARENT: Start working on making run fallback hide the file search one 2025-05-23 18:16:47 -07:00
Mike Griese
e19cdba074 PARENT: Update the fallback item too 2025-05-23 11:28:28 -07:00
Mike Griese
4223286061 PARENT: Expand env vars 2025-05-23 09:39:19 -07:00
Mike Griese
8e6bd141ca PARENT: Better deal with spaces 2025-05-23 09:37:29 -07:00
Mike Griese
d29121c3fd PARENT: logging that I don't think I needed 2025-05-18 19:00:54 -05:00
Mike Griese
58e0530980 Accept suggestions better 2025-05-18 19:00:41 -05:00
Mike Griese
9272e3112b This feels pretty great tbh 2025-05-18 18:47:10 -05:00
Mike Griese
a64095c3d3 try messing with selection to indicate text suggestion 2025-05-18 17:00:32 -05:00
Mike Griese
49480041cd this is near perfect 2025-05-18 15:43:28 -05:00
Mike Griese
547b664a8c Better handle removing the path item if the thing is an exe 2025-05-18 15:07:11 -05:00
Mike Griese
72320bea79 Add a single command for running the commandline. Remove history 2025-05-18 14:29:54 -05:00
Mike Griese
910de53a0a Merge remote-tracking branch 'origin/main' into dev/migrie/f/run-page-2 2025-05-18 10:18:20 -05:00
Mike Griese
11f60de543 stash: start moving into the main run page 2025-05-08 06:13:57 -05:00
Mike Griese
e7eb2d0239 Add the TextToSuggest back, just for demo purposes 2025-04-30 20:36:24 -05:00
Mike Griese
aefae2935e heck just use the original shell provider for history 2025-04-30 10:05:16 -05:00
Mike Griese
727367960e Start adding history 2025-04-30 06:01:45 -05:00
Mike Griese
32700658fd Deduplicate a bunch of code 2025-04-30 04:25:59 -05:00
Mike Griese
a7cb535515 What if the run page had the same typeahead that Rundlg had? 2025-04-29 17:01:49 -05:00
39 changed files with 1557 additions and 172 deletions

View File

@@ -282,3 +282,9 @@ xef
xes
PACKAGEVERSIONNUMBER
APPXMANIFESTVERSION
# MRU lists
CACHEWRITE
MRUCMPPROC
MRUINFO
REGSTR

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

@@ -56,6 +56,8 @@ public partial class ListViewModel : PageViewModel, IDisposable
public CommandItemViewModel EmptyContent { get; private set; }
public bool IsMainPage { get; init; }
private bool _isDynamic;
private Task? _initializeItemsTask;
@@ -370,6 +372,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
}
TextToSuggest = item.TextToSuggest;
WeakReferenceMessenger.Default.Send<UpdateSuggestionMessage>(new(item.TextToSuggest));
});
_lastSelectedItem = item;
@@ -423,6 +426,8 @@ public partial class ListViewModel : PageViewModel, IDisposable
WeakReferenceMessenger.Default.Send<HideDetailsMessage>();
WeakReferenceMessenger.Default.Send<UpdateSuggestionMessage>(new(string.Empty));
TextToSuggest = string.Empty;
});
}

View File

@@ -0,0 +1,9 @@
// 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.Core.ViewModels.Messages;
public record UpdateSuggestionMessage(string TextToSuggest)
{
}

View File

@@ -21,8 +21,12 @@ public partial class AppStateModel : ObservableObject
///////////////////////////////////////////////////////////////////////////
// STATE HERE
// Make sure that you make the setters public (JsonSerializer.Deserialize will fail silently otherwise)!
// Make sure that any new types you add are added to JsonSerializationContext!
public RecentCommandsManager RecentCommands { get; set; } = new();
public List<string> RunHistory { get; set; } = [];
// END SETTINGS
///////////////////////////////////////////////////////////////////////////
@@ -86,7 +90,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 +125,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

@@ -263,7 +263,7 @@ public partial class MainListPage : DynamicListPage,
{
nameMatch,
descriptionMatch,
isFallback ? 1 : 0, // Always give fallbacks a chance...
isFallback ? 1 : 0, // Always give fallbacks a chance
};
var max = scores.Max();
@@ -273,8 +273,7 @@ public partial class MainListPage : DynamicListPage,
// above "git" from "whatever"
max = max + extensionTitleMatch;
// ... but downweight them
var matchSomething = (max / (isFallback ? 3 : 1))
var matchSomething = max
+ (isAliasMatch ? 9001 : (isAliasSubstringMatch ? 1 : 0));
// If we matched title, subtitle, or alias (something real), then

View File

@@ -98,10 +98,13 @@ public partial class App : Application
// Built-in Commands. Order matters - this is the order they'll be presented by default.
var allApps = new AllAppsCommandProvider();
var files = new IndexerCommandsProvider();
files.SuppressFallbackWhen(ShellCommandsProvider.SuppressFileFallbackIf);
services.AddSingleton<ICommandProvider>(allApps);
services.AddSingleton<ICommandProvider, ShellCommandsProvider>();
services.AddSingleton<ICommandProvider, CalculatorCommandProvider>();
services.AddSingleton<ICommandProvider, IndexerCommandsProvider>();
services.AddSingleton<ICommandProvider>(files);
services.AddSingleton<ICommandProvider, BookmarksCommandProvider>();
services.AddSingleton<ICommandProvider, WindowWalkerCommandsProvider>();
@@ -144,6 +147,7 @@ public partial class App : Application
services.AddSingleton(state);
services.AddSingleton<IExtensionService, ExtensionService>();
services.AddSingleton<TrayIconService>();
services.AddSingleton<IRunHistoryService, RunHistoryService>();
services.AddSingleton<IRootPageService, PowerToysRootPageService>();
services.AddSingleton<IAppHostService, PowerToysAppHostService>();

View File

@@ -2,7 +2,6 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.WinUI;
using Microsoft.CmdPal.Core.ViewModels;
@@ -21,6 +20,7 @@ namespace Microsoft.CmdPal.UI.Controls;
public sealed partial class SearchBar : UserControl,
IRecipient<GoHomeMessage>,
IRecipient<FocusSearchBoxMessage>,
IRecipient<UpdateSuggestionMessage>,
ICurrentPageAware
{
private readonly DispatcherQueue _queue = DispatcherQueue.GetForCurrentThread();
@@ -31,6 +31,10 @@ public sealed partial class SearchBar : UserControl,
private readonly DispatcherQueueTimer _debounceTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
private bool _isBackspaceHeld;
private bool _inSuggestion;
private string? _lastText;
private string? _deletedSuggestion;
public PageViewModel? CurrentPageViewModel
{
get => (PageViewModel?)GetValue(CurrentPageViewModelProperty);
@@ -69,6 +73,7 @@ public sealed partial class SearchBar : UserControl,
this.InitializeComponent();
WeakReferenceMessenger.Default.Register<GoHomeMessage>(this);
WeakReferenceMessenger.Default.Register<FocusSearchBoxMessage>(this);
WeakReferenceMessenger.Default.Register<UpdateSuggestionMessage>(this);
}
public void ClearSearch()
@@ -125,15 +130,6 @@ public sealed partial class SearchBar : UserControl,
WeakReferenceMessenger.Default.Send<OpenContextMenuMessage>(new OpenContextMenuMessage(null, null, null, ContextMenuFilterLocation.Bottom));
e.Handled = true;
}
else if (e.Key == VirtualKey.Right)
{
if (CurrentPageViewModel != null && !string.IsNullOrEmpty(CurrentPageViewModel.TextToSuggest))
{
FilterBox.Text = CurrentPageViewModel.TextToSuggest;
FilterBox.Select(FilterBox.Text.Length, 0);
e.Handled = true;
}
}
else if (e.Key == VirtualKey.Escape)
{
if (string.IsNullOrEmpty(FilterBox.Text))
@@ -200,12 +196,65 @@ public sealed partial class SearchBar : UserControl,
e.Handled = true;
}
else if (e.Key == VirtualKey.Right)
{
if (_inSuggestion)
{
_inSuggestion = false;
_lastText = null;
DoFilterBoxUpdate();
}
}
else if (e.Key == VirtualKey.Down)
{
WeakReferenceMessenger.Default.Send<NavigateNextCommand>();
e.Handled = true;
}
if (_inSuggestion)
{
if (
e.Key == VirtualKey.Back ||
e.Key == VirtualKey.Delete
)
{
_deletedSuggestion = FilterBox.Text;
FilterBox.Text = _lastText ?? string.Empty;
FilterBox.Select(FilterBox.Text.Length, 0);
// Logger.LogInfo("deleting suggestion");
_inSuggestion = false;
_lastText = null;
e.Handled = true;
return;
}
var ignoreLeave =
e.Key == VirtualKey.Up ||
e.Key == VirtualKey.Down ||
e.Key == VirtualKey.RightMenu ||
e.Key == VirtualKey.LeftMenu ||
e.Key == VirtualKey.Menu ||
e.Key == VirtualKey.Shift ||
e.Key == VirtualKey.RightShift ||
e.Key == VirtualKey.LeftShift ||
e.Key == VirtualKey.RightControl ||
e.Key == VirtualKey.LeftControl ||
e.Key == VirtualKey.Control;
if (ignoreLeave)
{
return;
}
// Logger.LogInfo("leaving suggestion");
_inSuggestion = false;
_lastText = null;
}
}
private void FilterBox_PreviewKeyUp(object sender, KeyRoutedEventArgs e)
@@ -219,7 +268,7 @@ public sealed partial class SearchBar : UserControl,
private void FilterBox_TextChanged(object sender, TextChangedEventArgs e)
{
Debug.WriteLine($"FilterBox_TextChanged: {FilterBox.Text}");
// Logger.LogInfo($"FilterBox_TextChanged: {FilterBox.Text}");
// TERRIBLE HACK TODO GH #245
// There's weird wacky bugs with debounce currently. We're trying
@@ -228,23 +277,22 @@ public sealed partial class SearchBar : UserControl,
// (otherwise aliases just stop working)
if (FilterBox.Text.Length == 1)
{
if (CurrentPageViewModel != null)
{
CurrentPageViewModel.Filter = FilterBox.Text;
}
DoFilterBoxUpdate();
return;
}
if (_inSuggestion)
{
// Logger.LogInfo($"-- skipping, in suggestion --");
return;
}
// TODO: We could encapsulate this in a Behavior if we wanted to bind to the Filter property.
_debounceTimer.Debounce(
() =>
{
// Actually plumb Filtering to the view model
if (CurrentPageViewModel != null)
{
CurrentPageViewModel.Filter = FilterBox.Text;
}
DoFilterBoxUpdate();
},
//// Couldn't find a good recommendation/resource for value here. PT uses 50ms as default, so that is a reasonable default
//// This seems like a useful testing site for typing times: https://keyboardtester.info/keyboard-latency-test/
@@ -254,6 +302,21 @@ public sealed partial class SearchBar : UserControl,
immediate: FilterBox.Text.Length <= 1);
}
private void DoFilterBoxUpdate()
{
if (_inSuggestion)
{
// Logger.LogInfo($"--- skipping ---");
return;
}
// Actually plumb Filtering to the view model
if (CurrentPageViewModel != null)
{
CurrentPageViewModel.Filter = FilterBox.Text;
}
}
// Used to handle the case when a ListPage's `SearchText` may have changed
private void Page_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
@@ -273,6 +336,8 @@ public sealed partial class SearchBar : UserControl,
// ... Move the cursor to the end of the input
FilterBox.Select(FilterBox.Text.Length, 0);
}
// TODO! deal with suggestion
}
else if (property == nameof(ListViewModel.InitialSearchText))
{
@@ -290,4 +355,96 @@ public sealed partial class SearchBar : UserControl,
public void Receive(GoHomeMessage message) => ClearSearch();
public void Receive(FocusSearchBoxMessage message) => FilterBox.Focus(Microsoft.UI.Xaml.FocusState.Programmatic);
public void Receive(UpdateSuggestionMessage message)
{
var suggestion = message.TextToSuggest;
_queue.TryEnqueue(new(() =>
{
var clearSuggestion = string.IsNullOrEmpty(suggestion);
if (clearSuggestion && _inSuggestion)
{
// Logger.LogInfo($"Cleared suggestion \"{_lastText}\" to {suggestion}");
_inSuggestion = false;
FilterBox.Text = _lastText ?? string.Empty;
_lastText = null;
return;
}
if (clearSuggestion)
{
_deletedSuggestion = null;
return;
}
if (suggestion == _deletedSuggestion)
{
return;
}
else
{
_deletedSuggestion = null;
}
var currentText = _lastText ?? FilterBox.Text;
_lastText = currentText;
// if (_inSuggestion)
// {
// Logger.LogInfo($"Suggestion from \"{_lastText}\" to {suggestion}");
// }
// else
// {
// Logger.LogInfo($"Entering suggestion from \"{_lastText}\" to {suggestion}");
// }
_inSuggestion = true;
var matchedChars = 0;
var suggestionStartsWithQuote = suggestion.Length > 0 && suggestion[0] == '"';
var currentStartsWithQuote = currentText.Length > 0 && currentText[0] == '"';
var skipCheckingFirst = suggestionStartsWithQuote && !currentStartsWithQuote;
for (int i = skipCheckingFirst ? 1 : 0, j = 0;
i < suggestion.Length && j < currentText.Length;
i++, j++)
{
if (string.Equals(
suggestion[i].ToString(),
currentText[j].ToString(),
StringComparison.OrdinalIgnoreCase))
{
matchedChars++;
}
else
{
break;
}
}
var first = skipCheckingFirst ? "\"" : string.Empty;
var second = currentText.AsSpan(0, matchedChars);
var third = suggestion.AsSpan(matchedChars + (skipCheckingFirst ? 1 : 0));
var newText = string.Concat(
first,
second,
third);
FilterBox.Text = newText;
var wrappedInQuotes = suggestionStartsWithQuote && suggestion.Last() == '"';
if (wrappedInQuotes)
{
FilterBox.Select(
(skipCheckingFirst ? 1 : 0) + matchedChars,
Math.Max(0, suggestion.Length - matchedChars - 1 + (skipCheckingFirst ? -1 : 0)));
}
else
{
FilterBox.Select(matchedChars, suggestion.Length - matchedChars);
}
}));
}
}

View File

@@ -265,6 +265,13 @@ public sealed partial class ListPage : Page,
{
ItemsList.SelectedIndex = 0;
}
// Always reset the selected item when the top-level list page changes
// its items
if (!sender.IsNested)
{
ItemsList.SelectedIndex = 0;
}
}
private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)

View File

@@ -0,0 +1,50 @@
// 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;
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.RunHistory.CreateRunHistory();
_appStateModel.RunHistory.AddRange(history);
}
return _appStateModel.RunHistory;
}
public void ClearRunHistory()
{
_appStateModel.RunHistory.Clear();
}
public void AddRunHistoryItem(string item)
{
// insert at the beginning of the list
if (string.IsNullOrWhiteSpace(item))
{
return; // Do not add empty or whitespace items
}
_appStateModel.RunHistory.Remove(item);
// Add the item to the front of the history
_appStateModel.RunHistory.Insert(0, item);
AppStateModel.SaveState(_appStateModel);
}
}

View File

@@ -186,6 +186,8 @@
x:Load="False"
AutomationProperties.AccessibilityView="Raw"
CharacterSpacing="15"
FontFamily="{TemplateBinding FontFamily}"
FontSize="{TemplateBinding FontSize}"
Foreground="{Binding PlaceholderForeground, RelativeSource={RelativeSource TemplatedParent}, TargetNullValue={ThemeResource TextControlPlaceholderForeground}}"
Text="{TemplateBinding Description}"
TextWrapping="{TemplateBinding TextWrapping}" />

View File

@@ -383,4 +383,5 @@ namespace winrt::Microsoft::Terminal::UI::implementation
icon.Height(targetSize);
return icon;
}
}

View File

@@ -13,6 +13,7 @@ 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);
};
}

View File

@@ -153,6 +153,9 @@
<ClInclude Include="IconPathConverter.h">
<DependentUpon>IconPathConverter.idl</DependentUpon>
</ClInclude>
<ClInclude Include="RunHistory.h">
<DependentUpon>RunHistory.idl</DependentUpon>
</ClInclude>
<ClInclude Include="ResourceString.h">
<DependentUpon>ResourceString.idl</DependentUpon>
</ClInclude>
@@ -168,6 +171,9 @@
<ClCompile Include="IconPathConverter.cpp">
<DependentUpon>IconPathConverter.idl</DependentUpon>
</ClCompile>
<ClCompile Include="RunHistory.cpp">
<DependentUpon>RunHistory.idl</DependentUpon>
</ClCompile>
<ClCompile Include="ResourceString.cpp">
<DependentUpon>ResourceString.idl</DependentUpon>
</ClCompile>
@@ -176,6 +182,7 @@
<ItemGroup>
<Midl Include="Converters.idl" />
<Midl Include="IconPathConverter.idl" />
<Midl Include="RunHistory.idl" />
<Midl Include="IDirectKeyListener.idl" />
<Midl Include="ResourceString.idl" />
</ItemGroup>

View File

@@ -0,0 +1,87 @@
#include "pch.h"
#include "RunHistory.h"
#include "RunHistory.g.cpp"
using namespace winrt::Windows;
namespace winrt::Microsoft::Terminal::UI::implementation
{
// Run history
// Largely copied from the Run work circa 2022.
winrt::Windows::Foundation::Collections::IVector<hstring> RunHistory::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 hMruList = _createMRUList(&mi))
{
auto freeMRUList = wil::scope_exit([=]() {
_freeMRUList(hMruList);
});
for (int nMax = _enumMRUList(hMruList, -1, NULL, 0), i = 0; i < nMax; ++i)
{
WCHAR szCommand[MAX_PATH + 2];
const auto length = _enumMRUList(hMruList, 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

@@ -0,0 +1,21 @@
#pragma once
#include "RunHistory.g.h"
#include "types.h"
namespace winrt::Microsoft::Terminal::UI::implementation
{
struct RunHistory
{
RunHistory() = default;
static winrt::Windows::Foundation::Collections::IVector<hstring> CreateRunHistory();
private:
winrt::Windows::Foundation::Collections::IVector<hstring> _mruHistory;
};
}
namespace winrt::Microsoft::Terminal::UI::factory_implementation
{
BASIC_FACTORY(RunHistory);
}

View File

@@ -0,0 +1,11 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
namespace Microsoft.Terminal.UI
{
static runtimeclass RunHistory
{
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,23 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#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

@@ -30,13 +30,14 @@ public static class ResultHelper
var copyCommandItem = CreateResult(roundedResult, inputCulture, outputCulture, query);
// No TextToSuggest on the main save command item. We don't want to keep suggesting what the result is,
// as the user is typing it.
return new ListItem(saveCommand)
{
// Using CurrentCulture since this is user facing
Icon = Icons.ResultIcon,
Title = result,
Subtitle = query,
TextToSuggest = result,
MoreCommands = [
new CommandContextItem(copyCommandItem.Command)
{

View File

@@ -23,6 +23,8 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, System
private uint _queryCookie = 10;
private Func<string, bool> _suppressCallback;
public FallbackOpenFileItem()
: base(_baseCommandWithId, Resources.Indexer_Find_Path_fallback_display_title)
{
@@ -44,6 +46,17 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, System
return;
}
if (_suppressCallback != null && _suppressCallback(query))
{
Command = new NoOpCommand();
Title = string.Empty;
Subtitle = string.Empty;
Icon = null;
MoreCommands = null;
return;
}
if (Path.Exists(query))
{
// Exit 1: The query is a direct path to a file. Great! Return it.
@@ -128,4 +141,9 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, System
_searchEngine.Dispose();
GC.SuppressFinalize(this);
}
public void SuppressFallbackWhen(Func<string, bool> callback)
{
_suppressCallback = callback;
}
}

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 System;
using Microsoft.CmdPal.Ext.Indexer.Data;
using Microsoft.CmdPal.Ext.Indexer.Properties;
using Microsoft.CommandPalette.Extensions;
@@ -41,4 +42,9 @@ public partial class IndexerCommandsProvider : CommandProvider
[
_fallbackFileItem
];
public void SuppressFallbackWhen(Func<string, bool> callback)
{
_fallbackFileItem.SuppressFallbackWhen(callback);
}
}

View File

@@ -36,7 +36,7 @@ internal sealed partial class ExecuteItem : InvokableCommand
else
{
Name = Properties.Resources.generic_run_command;
Icon = Icons.ReturnIcon;
Icon = Icons.RunV2Icon;
}
Cmd = cmd;
@@ -44,36 +44,6 @@ internal sealed partial class ExecuteItem : InvokableCommand
_runas = type;
}
private static bool ExistInPath(string filename)
{
if (File.Exists(filename))
{
return true;
}
else
{
var values = Environment.GetEnvironmentVariable("PATH");
if (values != null)
{
foreach (var path in values.Split(';'))
{
var path1 = Path.Combine(path, filename);
var path2 = Path.Combine(path, filename + ".exe");
if (File.Exists(path1) || File.Exists(path2))
{
return true;
}
}
return false;
}
else
{
return false;
}
}
}
private void Execute(Func<ProcessStartInfo, Process?> startProcess, ProcessStartInfo info)
{
if (startProcess == null)
@@ -184,7 +154,7 @@ internal sealed partial class ExecuteItem : InvokableCommand
if (parts.Length == 2)
{
var filename = parts[0];
if (ExistInPath(filename))
if (ShellListPageHelpers.FileExistInPath(filename))
{
var arguments = parts[1];
if (_settings.LeaveShellOpen)

View File

@@ -2,39 +2,199 @@
// 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.Ext.Shell.Commands;
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
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;
internal sealed partial class FallbackExecuteItem : FallbackCommandItem
internal sealed partial class FallbackExecuteItem : FallbackCommandItem, IDisposable
{
private readonly ExecuteItem _executeItem;
private readonly SettingsManager _settings;
private readonly Action<string>? _addToHistory;
private CancellationTokenSource? _cancellationTokenSource;
private Task? _currentUpdateTask;
public FallbackExecuteItem(SettingsManager settings)
public FallbackExecuteItem(SettingsManager settings, Action<string>? addToHistory)
: base(
new ExecuteItem(string.Empty, settings) { Id = "com.microsoft.run.fallback" },
new NoOpCommand() { Id = "com.microsoft.run.fallback" },
Resources.shell_command_display_title)
{
_settings = settings;
_executeItem = (ExecuteItem)this.Command!;
Title = string.Empty;
_executeItem.Name = string.Empty;
Subtitle = Properties.Resources.generic_run_command;
Icon = Icons.RunV2Icon; // Defined in Icons.cs and contains the execute command icon.
_addToHistory = addToHistory;
}
public override void UpdateQuery(string query)
{
_executeItem.Cmd = query;
_executeItem.Name = string.IsNullOrEmpty(query) ? string.Empty : Properties.Resources.generic_run_command;
Title = query;
MoreCommands = [
new CommandContextItem(new ExecuteItem(query, _settings, RunAsType.Administrator)),
new CommandContextItem(new ExecuteItem(query, _settings, RunAsType.OtherUser)),
];
// Cancel any ongoing query processing
_cancellationTokenSource?.Cancel();
_cancellationTokenSource = new CancellationTokenSource();
var cancellationToken = _cancellationTokenSource.Token;
try
{
// Save the latest update task
_currentUpdateTask = DoUpdateQueryAsync(query, cancellationToken);
}
catch (OperationCanceledException)
{
// DO NOTHING HERE
return;
}
catch (Exception)
{
// Handle other exceptions
return;
}
// Await the task to ensure only the latest one gets processed
_ = ProcessUpdateResultsAsync(_currentUpdateTask);
}
private async Task ProcessUpdateResultsAsync(Task updateTask)
{
try
{
await updateTask;
}
catch (OperationCanceledException)
{
// Handle cancellation gracefully
}
catch (Exception)
{
// Handle other exceptions
}
}
private async Task DoUpdateQueryAsync(string query, CancellationToken cancellationToken)
{
// Check for cancellation at the start
cancellationToken.ThrowIfCancellationRequested();
var searchText = query.Trim();
var expanded = Environment.ExpandEnvironmentVariables(searchText);
searchText = expanded;
if (string.IsNullOrEmpty(searchText) || string.IsNullOrWhiteSpace(searchText))
{
Command = null;
Title = string.Empty;
return;
}
ShellListPage.ParseExecutableAndArgs(searchText, out var exe, out var args);
// Check for cancellation before file system operations
cancellationToken.ThrowIfCancellationRequested();
var exeExists = false;
var fullExePath = string.Empty;
var pathIsDir = false;
try
{
// Create a timeout for file system operations (200ms)
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromMilliseconds(200));
using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
var timeoutToken = combinedCts.Token;
// Use Task.Run with timeout for file system operations
var fileSystemTask = Task.Run(
() =>
{
exeExists = ShellListPageHelpers.FileExistInPath(exe, out fullExePath);
pathIsDir = Directory.Exists(exe);
},
CancellationToken.None);
// Wait for either completion or timeout
await fileSystemTask.WaitAsync(timeoutToken);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
// Main cancellation token was cancelled, re-throw
throw;
}
catch (TimeoutException)
{
// Timeout occurred - use defaults
return;
}
catch (OperationCanceledException)
{
// Timeout occurred (from WaitAsync) - use defaults
return;
}
catch (Exception)
{
// Handle any other exceptions that might bubble up
return;
}
// Check for cancellation before updating UI properties
cancellationToken.ThrowIfCancellationRequested();
if (exeExists)
{
// TODO we need to probably get rid of the settings for this provider entirely
var exeItem = ShellListPage.CreateExeItem(exe, args, fullExePath, _addToHistory);
Title = exeItem.Title;
Subtitle = exeItem.Subtitle;
Icon = exeItem.Icon;
Command = exeItem.Command;
MoreCommands = exeItem.MoreCommands;
}
else if (pathIsDir)
{
var pathItem = new PathListItem(exe, query, _addToHistory);
Title = pathItem.Title;
Subtitle = pathItem.Subtitle;
Icon = pathItem.Icon;
Command = pathItem.Command;
MoreCommands = pathItem.MoreCommands;
}
else if (System.Uri.TryCreate(searchText, UriKind.Absolute, out var uri))
{
Command = new OpenUrlWithHistoryCommand(searchText, _addToHistory) { Result = CommandResult.Dismiss() };
Title = searchText;
}
else
{
Command = null;
Title = string.Empty;
}
// Final cancellation check
cancellationToken.ThrowIfCancellationRequested();
}
public void Dispose()
{
_cancellationTokenSource?.Cancel();
_cancellationTokenSource?.Dispose();
}
internal static bool SuppressFileFallbackIf(string query)
{
var searchText = query.Trim();
var expanded = Environment.ExpandEnvironmentVariables(searchText);
searchText = expanded;
if (string.IsNullOrEmpty(searchText) || string.IsNullOrWhiteSpace(searchText))
{
return false;
}
ShellListPage.ParseExecutableAndArgs(searchText, out var exe, out var args);
var exeExists = ShellListPageHelpers.FileExistInPath(exe, out var fullExePath);
var pathIsDir = Directory.Exists(exe);
return exeExists || pathIsDir;
}
}

View File

@@ -4,12 +4,12 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CmdPal.Ext.Shell.Commands;
using Microsoft.CmdPal.Ext.Shell.Pages;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Shell.Helpers;
@@ -26,7 +26,7 @@ public class ShellListPageHelpers
private ListItem GetCurrentCmd(string cmd)
{
ListItem result = new ListItem(new ExecuteItem(cmd, _settings))
var result = new ListItem(new ExecuteItem(cmd, _settings))
{
Title = cmd,
Subtitle = Properties.Resources.cmd_plugin_name + ": " + Properties.Resources.cmd_execute_through_shell,
@@ -36,58 +36,6 @@ public class ShellListPageHelpers
return result;
}
private List<ListItem> GetHistoryCmds(string cmd, ListItem result)
{
IEnumerable<ListItem?> history = _settings.Count.Where(o => o.Key.Contains(cmd, StringComparison.CurrentCultureIgnoreCase))
.OrderByDescending(o => o.Value)
.Select(m =>
{
if (m.Key == cmd)
{
// Using CurrentCulture since this is user facing
result.Subtitle = Properties.Resources.cmd_plugin_name + ": " + string.Format(CultureInfo.CurrentCulture, CmdHasBeenExecutedTimes, m.Value);
return null;
}
var ret = new ListItem(new ExecuteItem(m.Key, _settings))
{
Title = m.Key,
// Using CurrentCulture since this is user facing
Subtitle = Properties.Resources.cmd_plugin_name + ": " + string.Format(CultureInfo.CurrentCulture, CmdHasBeenExecutedTimes, m.Value),
Icon = Icons.HistoryIcon,
};
return ret;
}).Where(o => o != null).Take(4);
return history.Select(o => o!).ToList();
}
public List<ListItem> Query(string query)
{
ArgumentNullException.ThrowIfNull(query);
List<ListItem> results = new List<ListItem>();
var cmd = query;
if (string.IsNullOrEmpty(cmd))
{
results = ResultsFromHistory();
}
else
{
var queryCmd = GetCurrentCmd(cmd);
results.Add(queryCmd);
var history = GetHistoryCmds(cmd, queryCmd);
results.AddRange(history);
}
foreach (var currItem in results)
{
currItem.MoreCommands = LoadContextMenus(currItem).ToArray();
}
return results;
}
public List<CommandContextItem> LoadContextMenus(ListItem listItem)
{
var resultList = new List<CommandContextItem>
@@ -99,18 +47,129 @@ public class ShellListPageHelpers
return resultList;
}
private List<ListItem> ResultsFromHistory()
internal static bool FileExistInPath(string filename)
{
IEnumerable<ListItem> history = _settings.Count.OrderByDescending(o => o.Value)
.Select(m => new ListItem(new ExecuteItem(m.Key, _settings))
return FileExistInPath(filename, out var _);
}
internal static bool FileExistInPath(string filename, out string fullPath, CancellationToken? token = null)
{
fullPath = string.Empty;
if (File.Exists(filename))
{
token?.ThrowIfCancellationRequested();
fullPath = Path.GetFullPath(filename);
return true;
}
else
{
var values = Environment.GetEnvironmentVariable("PATH");
if (values != null)
{
Title = m.Key,
foreach (var path in values.Split(';'))
{
var path1 = Path.Combine(path, filename);
if (File.Exists(path1))
{
fullPath = Path.GetFullPath(path1);
return true;
}
// Using CurrentCulture since this is user facing
Subtitle = Properties.Resources.cmd_plugin_name + ": " + string.Format(CultureInfo.CurrentCulture, CmdHasBeenExecutedTimes, m.Value),
Icon = Icons.HistoryIcon,
}).Take(5);
token?.ThrowIfCancellationRequested();
return history.ToList();
var path2 = Path.Combine(path, filename + ".exe");
if (File.Exists(path2))
{
fullPath = Path.GetFullPath(path2);
return true;
}
token?.ThrowIfCancellationRequested();
}
return false;
}
else
{
return false;
}
}
}
internal static ListItem? ListItemForCommandString(string query, Action<string>? addToHistory)
{
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 = false;
var pathIsDir = false;
var fullExePath = string.Empty;
try
{
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(200));
// Use Task.Run with timeout - this will actually timeout even if the sync operations don't respond to cancellation
var pathResolutionTask = Task.Run(
() =>
{
// Don't check cancellation token here - let the Task timeout handle it
exeExists = ShellListPageHelpers.FileExistInPath(exe, out fullExePath);
pathIsDir = Directory.Exists(expanded);
},
CancellationToken.None); // Use None here since we're handling timeout differently
// Wait for either completion or timeout
pathResolutionTask.Wait(cts.Token);
}
catch (OperationCanceledException)
{
}
if (exeExists)
{
// TODO we need to probably get rid of the settings for this provider entirely
var exeItem = ShellListPage.CreateExeItem(exe, args, fullExePath, addToHistory);
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, addToHistory);
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 OpenUrlWithHistoryCommand(searchText) { Result = CommandResult.Dismiss() };
li.Title = searchText;
}
else
{
return null;
}
if (li != null)
{
li.TextToSuggest = searchText;
}
return li;
}
}

View File

@@ -10,11 +10,9 @@ internal sealed class Icons
{
internal static IconInfo RunV2Icon { get; } = IconHelpers.FromRelativePath("Assets\\Run.svg");
internal static IconInfo HistoryIcon { get; } = new IconInfo("\uE81C"); // History
internal static IconInfo FolderIcon { get; } = new IconInfo("📁");
internal static IconInfo AdminIcon { get; } = new IconInfo("\xE7EF"); // Admin Icon
internal static IconInfo UserIcon { get; } = new IconInfo("\xE7EE"); // User Icon
internal static IconInfo ReturnIcon { get; } = new IconInfo("\uE751"); // Return Key Icon
}

View File

@@ -14,7 +14,12 @@
</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" />
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DependentUpon>Resources.resx</DependentUpon>

View File

@@ -0,0 +1,30 @@
// 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.IO;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Shell;
internal sealed partial class OpenUrlWithHistoryCommand : OpenUrlCommand
{
private readonly Action<string>? _addToHistory;
private readonly string _url;
public OpenUrlWithHistoryCommand(string url, Action<string>? addToHistory = null)
: base(url)
{
_addToHistory = addToHistory;
_url = url;
}
public override CommandResult Invoke()
{
_addToHistory?.Invoke(_url);
var result = base.Invoke();
return result;
}
}

View File

@@ -0,0 +1,115 @@
// 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.Threading.Tasks;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Storage.Streams;
namespace Microsoft.CmdPal.Ext.Shell.Pages;
internal sealed partial class RunExeItem : ListItem
{
private readonly Lazy<IconInfo> _icon;
private readonly Action<string>? _addToHistory;
public override IIconInfo? Icon { get => _icon.Value; set => base.Icon = value; }
internal string FullExePath { get; private set; }
internal string Exe { get; private set; }
private string _args = string.Empty;
private string FullString => string.IsNullOrEmpty(_args) ? Exe : $"{Exe} {_args}";
public RunExeItem(string exe, string args, string fullExePath, Action<string>? addToHistory)
{
FullExePath = fullExePath;
Exe = exe;
var command = new AnonymousCommand(Run)
{
Name = Properties.Resources.generic_run_command,
Result = CommandResult.Dismiss(),
};
Command = command;
Subtitle = FullExePath;
_icon = new Lazy<IconInfo>(() =>
{
var t = FetchIcon();
t.Wait();
return t.Result;
});
_addToHistory = addToHistory;
UpdateArgs(args);
MoreCommands = [
new CommandContextItem(
new AnonymousCommand(RunAsAdmin)
{
Name = Properties.Resources.cmd_run_as_administrator,
Icon = Icons.AdminIcon,
}),
new CommandContextItem(
new AnonymousCommand(RunAsOther)
{
Name = Properties.Resources.cmd_run_as_user,
Icon = Icons.UserIcon,
}),
];
}
internal void UpdateArgs(string args)
{
_args = args;
Title = string.IsNullOrEmpty(_args) ? Exe : Exe + " " + _args; // todo! you're smarter than this
}
public async Task<IconInfo> FetchIcon()
{
IconInfo? icon = null;
try
{
var stream = await ThumbnailHelper.GetThumbnail(FullExePath);
if (stream != null)
{
var data = new IconData(RandomAccessStreamReference.CreateFromStream(stream));
icon = new IconInfo(data, data);
((AnonymousCommand?)Command)!.Icon = icon;
}
}
catch
{
}
icon = icon ?? new IconInfo(FullExePath);
return icon;
}
public void Run()
{
_addToHistory?.Invoke(FullString);
ShellHelpers.OpenInShell(FullExePath, _args);
}
public void RunAsAdmin()
{
_addToHistory?.Invoke(FullString);
ShellHelpers.OpenInShell(FullExePath, _args, runAs: ShellHelpers.ShellRunAsType.Administrator);
}
public void RunAsOther()
{
_addToHistory?.Invoke(FullString);
ShellHelpers.OpenInShell(FullExePath, _args, runAs: ShellHelpers.ShellRunAsType.OtherUser);
}
}

View File

@@ -2,6 +2,13 @@
// 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.Common.Services;
using Microsoft.CmdPal.Ext.Shell.Helpers;
using Microsoft.CmdPal.Ext.Shell.Properties;
using Microsoft.CommandPalette.Extensions;
@@ -9,20 +16,521 @@ using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Shell.Pages;
internal sealed partial class ShellListPage : DynamicListPage
internal sealed partial class ShellListPage : DynamicListPage, IDisposable
{
private readonly ShellListPageHelpers _helper;
public ShellListPage(SettingsManager settingsManager)
private readonly List<ListItem> _topLevelItems = [];
private readonly Dictionary<string, ListItem> _historyItems = [];
private readonly List<ListItem> _currentHistoryItems = [];
private readonly IRunHistoryService _historyService;
private RunExeItem? _exeItem;
private List<ListItem> _pathItems = [];
private ListItem? _uriItem;
private CancellationTokenSource? _cancellationTokenSource;
private Task? _currentSearchTask;
private bool _loadedInitialHistory;
public ShellListPage(SettingsManager settingsManager, IRunHistoryService runHistoryService, bool addBuiltins = false)
{
Icon = Icons.RunV2Icon;
Id = "com.microsoft.cmdpal.shell";
Name = Resources.cmd_plugin_name;
PlaceholderText = Resources.list_placeholder_text;
_helper = new(settingsManager);
_historyService = runHistoryService;
EmptyContent = new CommandItem()
{
Title = Resources.cmd_plugin_name,
Icon = Icons.RunV2Icon,
Subtitle = Resources.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) => RaiseItemsChanged(0);
public override void UpdateSearchText(string oldSearch, string newSearch)
{
if (newSearch == oldSearch)
{
return;
}
public override IListItem[] GetItems() => [.. _helper.Query(SearchText)];
DoUpdateSearchText(newSearch);
}
private void DoUpdateSearchText(string newSearch)
{
// Cancel any ongoing search
_cancellationTokenSource?.Cancel();
_cancellationTokenSource = new CancellationTokenSource();
var cancellationToken = _cancellationTokenSource.Token;
try
{
// Save the latest search task
_currentSearchTask = BuildListItemsForSearchAsync(newSearch, cancellationToken);
}
catch (OperationCanceledException)
{
// DO NOTHING HERE
return;
}
catch (Exception)
{
// Handle other exceptions
return;
}
// Await the task to ensure only the latest one gets processed
_ = ProcessSearchResultsAsync(_currentSearchTask, newSearch);
}
private async Task ProcessSearchResultsAsync(Task searchTask, string newSearch)
{
try
{
await searchTask;
// Ensure this is still the latest task
if (_currentSearchTask == searchTask)
{
// The search results have already been updated in BuildListItemsForSearchAsync
IsLoading = false;
RaiseItemsChanged();
}
}
catch (OperationCanceledException)
{
// Handle cancellation gracefully
}
catch (Exception)
{
// Handle other exceptions
IsLoading = false;
}
}
private async Task BuildListItemsForSearchAsync(string newSearch, CancellationToken cancellationToken)
{
// Check for cancellation at the start
cancellationToken.ThrowIfCancellationRequested();
// 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:
// 1. Check if the search text is a valid path
// 2. If it is, then list all the files that start with that text
var searchText = newSearch.Trim();
var expanded = Environment.ExpandEnvironmentVariables(searchText);
// Check for cancellation after environment expansion
cancellationToken.ThrowIfCancellationRequested();
// TODO we can be smarter about only re-reading the filesystem if the
// new search is just the oldSearch+some chars
if (string.IsNullOrEmpty(searchText) || string.IsNullOrWhiteSpace(searchText))
{
_pathItems.Clear();
_exeItem = null;
_uriItem = null;
_currentHistoryItems.Clear();
_currentHistoryItems.AddRange(_historyItems.Values);
return;
}
ParseExecutableAndArgs(expanded, out var exe, out var args);
// Check for cancellation before file system operations
cancellationToken.ThrowIfCancellationRequested();
// Reset the path resolution flag
var couldResolvePath = false;
var exeExists = false;
var fullExePath = string.Empty;
var pathIsDir = false;
try
{
// Create a timeout for file system operations (200ms)
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromMilliseconds(200));
using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
var timeoutToken = combinedCts.Token;
// Use Task.Run with timeout - this will actually timeout even if the sync operations don't respond to cancellation
var pathResolutionTask = Task.Run(
() =>
{
// Don't check cancellation token here - let the Task timeout handle it
exeExists = ShellListPageHelpers.FileExistInPath(exe, out fullExePath);
pathIsDir = Directory.Exists(expanded);
couldResolvePath = true;
},
CancellationToken.None); // Use None here since we're handling timeout differently
// Wait for either completion or timeout
await pathResolutionTask.WaitAsync(timeoutToken);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
// Main cancellation token was cancelled, re-throw
throw;
}
catch (TimeoutException)
{
// Timeout occurred
couldResolvePath = false;
}
catch (OperationCanceledException)
{
// Timeout occurred (from WaitAsync)
couldResolvePath = false;
}
catch (Exception)
{
// Handle any other exceptions that might bubble up
couldResolvePath = false;
}
cancellationToken.ThrowIfCancellationRequested();
_pathItems.Clear();
// We want to show path items:
// * If there's no args, AND (the path doesn't exist OR the path is a dir)
if (string.IsNullOrEmpty(args)
&& (!exeExists || pathIsDir)
&& couldResolvePath)
{
IsLoading = true;
await CreatePathItemsAsync(expanded, searchText, cancellationToken);
}
// Check for cancellation before creating exe items
cancellationToken.ThrowIfCancellationRequested();
if (couldResolvePath && exeExists)
{
CreateAndAddExeItems(exe, args, fullExePath);
}
else
{
_exeItem = null;
}
// Only create the URI item if we didn't make a file or exe item for it.
if (!exeExists && !pathIsDir)
{
CreateUriItems(searchText);
}
else
{
_uriItem = null;
}
var histItemsNotInSearch =
_historyItems
.Where(kv => !kv.Key.Equals(newSearch, StringComparison.OrdinalIgnoreCase));
if (_exeItem != null)
{
// If we have an exe item, we want to remove it from the history items
histItemsNotInSearch = histItemsNotInSearch
.Where(kv => !kv.Value.Title.Equals(_exeItem.Title, StringComparison.OrdinalIgnoreCase));
}
if (_uriItem != null)
{
// If we have an uri item, we want to remove it from the history items
histItemsNotInSearch = histItemsNotInSearch
.Where(kv => !kv.Value.Title.Equals(_uriItem.Title, StringComparison.OrdinalIgnoreCase));
}
// Filter the history items based on the search text
var filterHistory = (string query, KeyValuePair<string, ListItem> pair) =>
{
// Fuzzy search on the key (command string)
var score = StringMatcher.FuzzySearch(query, pair.Key).Score;
return score;
};
var filteredHistory =
ListHelpers.FilterList<KeyValuePair<string, ListItem>>(
histItemsNotInSearch,
searchText,
filterHistory)
.Select(p => p.Value);
_currentHistoryItems.Clear();
_currentHistoryItems.AddRange(filteredHistory);
// Final cancellation check
cancellationToken.ThrowIfCancellationRequested();
}
private static ListItem PathToListItem(string path, string originalPath, string args = "", Action<string>? addToHistory = null)
{
var pathItem = new PathListItem(path, originalPath, addToHistory);
// Is this path an executable? If so, then make a RunExeItem
if (IsExecutable(path))
{
var exeItem = new RunExeItem(Path.GetFileName(path), args, path, addToHistory);
exeItem.MoreCommands = [
.. exeItem.MoreCommands,
.. pathItem.MoreCommands];
return exeItem;
}
return pathItem;
}
public override IListItem[] GetItems()
{
if (!_loadedInitialHistory)
{
LoadInitialHistory();
}
var filteredTopLevel = ListHelpers.FilterList(_topLevelItems, SearchText);
List<ListItem> uriItems = _uriItem != null ? [_uriItem] : [];
List<ListItem> exeItems = _exeItem != null ? [_exeItem] : [];
return
exeItems
.Concat(filteredTopLevel)
.Concat(_currentHistoryItems)
.Concat(_pathItems)
.Concat(uriItems)
.ToArray();
}
internal static RunExeItem CreateExeItem(string exe, string args, string fullExePath, Action<string>? addToHistory)
{
// 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) as RunExeItem ??
new RunExeItem(exe, args, fullExePath, addToHistory);
}
private void CreateAndAddExeItems(string exe, string args, string fullExePath)
{
// If we already have an exe item, and the exe is the same, we can just update it
if (_exeItem != null && _exeItem.FullExePath.Equals(fullExePath, StringComparison.OrdinalIgnoreCase))
{
_exeItem.UpdateArgs(args);
}
else
{
_exeItem = CreateExeItem(exe, args, fullExePath, AddToHistory);
}
}
private static bool IsExecutable(string path)
{
// Is this path an executable?
// check all the extensions in PATHEXT
var extensions = Environment.GetEnvironmentVariable("PATHEXT")?.Split(';') ?? Array.Empty<string>();
return extensions.Any(ext => string.Equals(Path.GetExtension(path), ext, StringComparison.OrdinalIgnoreCase));
}
private async Task CreatePathItemsAsync(string searchPath, string originalPath, CancellationToken cancellationToken)
{
var directoryPath = string.Empty;
var searchPattern = string.Empty;
var startsWithQuote = searchPath.Length > 0 && searchPath[0] == '"';
var endsWithQuote = searchPath.Last() == '"';
var trimmed = (startsWithQuote && endsWithQuote) ? searchPath.Substring(1, searchPath.Length - 2) : searchPath;
var isDriveRoot = trimmed.Length == 2 && trimmed[1] == ':';
// we should also handle just drive roots, ala c:\ or d:\
// we need to handle this case first, because "C:" does exist, but we need to append the "\" in that case
if (isDriveRoot)
{
directoryPath = trimmed + "\\";
searchPattern = $"*";
}
// Easiest case: text is literally already a full directory
else if (Directory.Exists(trimmed))
{
directoryPath = trimmed;
searchPattern = $"*";
}
// Check if the search text is a valid path
else if (Path.IsPathRooted(trimmed) && Path.GetDirectoryName(trimmed) is string directoryName)
{
directoryPath = directoryName;
searchPattern = $"{Path.GetFileName(trimmed)}*";
}
// Check if the search text is a valid UNC path
else if (trimmed.StartsWith(@"\\", System.StringComparison.CurrentCultureIgnoreCase) &&
trimmed.Contains(@"\\"))
{
directoryPath = trimmed;
searchPattern = $"*";
}
// Check for cancellation before directory operations
cancellationToken.ThrowIfCancellationRequested();
var dirExists = Directory.Exists(directoryPath);
// searchPath is fully expanded, and originalPath is not. We might get:
// * original: X%Y%Z\partial
// * search: X_foo_Z\partial
// and we want the result `X_foo_Z\partialOne` to use the suggestion `X%Y%Z\partialOne`
//
// To do this:
// * Get the directoryPath
// * trim that out of the beginning of searchPath -> searchPathTrailer
// * everything left from searchPath? remove searchPathTrailer from the end of originalPath
// that gets us the expanded original dir
// Check if the directory exists
if (dirExists)
{
// Check for cancellation before file system enumeration
cancellationToken.ThrowIfCancellationRequested();
// 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();
var searchPathTrailer = trimmed.Remove(0, Math.Min(directoryPath.Length, trimmed.Length));
var originalBeginning = originalPath.Remove(originalPath.Length - searchPathTrailer.Length);
if (isDriveRoot)
{
originalBeginning = string.Concat(originalBeginning, '\\');
}
// Create a list of commands for each file
var commands = files.Select(f => PathToListItem(f, originalBeginning)).ToList();
// Final cancellation check before updating results
cancellationToken.ThrowIfCancellationRequested();
// Add the commands to the list
_pathItems = commands;
}
else
{
_pathItems.Clear();
}
}
internal static void ParseExecutableAndArgs(string input, out string executable, out string arguments)
{
input = input.Trim();
executable = string.Empty;
arguments = string.Empty;
if (string.IsNullOrEmpty(input))
{
return;
}
if (input.StartsWith("\"", System.StringComparison.InvariantCultureIgnoreCase))
{
// Find the closing quote
var closingQuoteIndex = input.IndexOf('\"', 1);
if (closingQuoteIndex > 0)
{
executable = input.Substring(1, closingQuoteIndex - 1);
if (closingQuoteIndex + 1 < input.Length)
{
arguments = input.Substring(closingQuoteIndex + 1).TrimStart();
}
}
}
else
{
// Executable ends at first space
var firstSpaceIndex = input.IndexOf(' ');
if (firstSpaceIndex > 0)
{
executable = input.Substring(0, firstSpaceIndex);
arguments = input[(firstSpaceIndex + 1)..].TrimStart();
}
else
{
executable = input;
}
}
}
internal void CreateUriItems(string searchText)
{
if (!System.Uri.TryCreate(searchText, UriKind.Absolute, out var uri))
{
_uriItem = null;
return;
}
var command = new OpenUrlCommand(searchText) { Result = CommandResult.Dismiss() };
_uriItem = new ListItem(command)
{
Title = searchText,
};
}
private void LoadInitialHistory()
{
var hist = _historyService.GetRunHistory();
var histItems = hist
.Select(h => (h, ShellListPageHelpers.ListItemForCommandString(h, AddToHistory)))
.Where(tuple => tuple.Item2 != null)
.Select(tuple => (tuple.h, tuple.Item2!))
.ToList();
_historyItems.Clear();
// Add all the history items to the _historyItems dictionary
foreach (var (h, item) in histItems)
{
_historyItems[h] = item;
}
_currentHistoryItems.Clear();
_currentHistoryItems.AddRange(histItems.Select(tuple => tuple.Item2));
_loadedInitialHistory = true;
}
internal void AddToHistory(string commandString)
{
if (string.IsNullOrWhiteSpace(commandString))
{
return; // Do not add empty or whitespace items
}
_historyService.AddRunHistoryItem(commandString);
LoadInitialHistory();
DoUpdateSearchText(SearchText);
}
public void Dispose()
{
_cancellationTokenSource?.Cancel();
_cancellationTokenSource?.Dispose();
}
}

View File

@@ -0,0 +1,69 @@
// 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.IO;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Shell;
internal sealed partial class PathListItem : ListItem
{
private readonly Lazy<IconInfo> _icon;
private readonly bool _isDirectory;
public override IIconInfo? Icon { get => _icon.Value; set => base.Icon = value; }
public PathListItem(string path, string originalDir, Action<string>? addToHistory)
: base(new OpenUrlWithHistoryCommand(path, addToHistory))
{
var fileName = Path.GetFileName(path);
_isDirectory = Directory.Exists(path);
if (_isDirectory)
{
path = path + "\\";
fileName = fileName + "\\";
}
Title = fileName;
Subtitle = path;
// NOTE ME:
// If there are spaces on originalDir, trim them off, BEFORE combining originalDir and fileName.
// THEN add quotes at the end
// Trim off leading & trailing quote, if there is one
var trimmed = originalDir.Trim('"');
var originalPath = Path.Combine(trimmed, fileName);
var suggestion = originalPath;
var hasSpace = originalPath.Contains(' ');
if (hasSpace)
{
// wrap it in quotes
suggestion = string.Concat("\"", suggestion, "\"");
}
TextToSuggest = suggestion;
MoreCommands = [
new CommandContextItem(new CopyTextCommand(path) { Name = Properties.Resources.copy_path_command_name }) { }
];
// TODO: Follow-up during 0.4. Add the indexer commands here.
// MoreCommands = [
// new CommandContextItem(new OpenWithCommand(indexerItem)),
// new CommandContextItem(new ShowFileInFolderCommand(indexerItem.FullPath) { Name = Resources.Indexer_Command_ShowInFolder }),
// new CommandContextItem(new CopyPathCommand(indexerItem)),
// new CommandContextItem(new OpenInConsoleCommand(indexerItem)),
// new CommandContextItem(new OpenPropertiesCommand(indexerItem)),
// ];
_icon = new Lazy<IconInfo>(() =>
{
var iconStream = ThumbnailHelper.GetThumbnail(path).Result;
var icon = iconStream != null ? IconInfo.FromStream(iconStream) :
_isDirectory ? Icons.FolderIcon : Icons.RunV2Icon;
return icon;
});
}
}

View File

@@ -132,6 +132,15 @@ namespace Microsoft.CmdPal.Ext.Shell.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Copy path.
/// </summary>
public static string copy_path_command_name {
get {
return ResourceManager.GetString("copy_path_command_name", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Find and run the executable file.
/// </summary>

View File

@@ -190,4 +190,7 @@
<data name="shell_command_display_title" xml:space="preserve">
<value>Run commands</value>
</data>
<data name="copy_path_command_name" xml:space="preserve">
<value>Copy path</value>
</data>
</root>

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;
@@ -13,19 +14,26 @@ namespace Microsoft.CmdPal.Ext.Shell;
public partial class ShellCommandsProvider : CommandProvider
{
private readonly CommandItem _shellPageItem;
private readonly SettingsManager _settingsManager = new();
private readonly FallbackCommandItem _fallbackItem;
public ShellCommandsProvider()
private readonly SettingsManager _settingsManager = new();
private readonly ShellListPage _shellListPage;
private readonly FallbackCommandItem _fallbackItem;
private readonly IRunHistoryService _historyService;
public ShellCommandsProvider(IRunHistoryService runHistoryService)
{
_historyService = runHistoryService;
Id = "Run";
DisplayName = Resources.cmd_plugin_name;
Icon = Icons.RunV2Icon;
Settings = _settingsManager.Settings;
_fallbackItem = new FallbackExecuteItem(_settingsManager);
_shellListPage = new ShellListPage(_settingsManager, _historyService);
_shellPageItem = new CommandItem(new ShellListPage(_settingsManager))
_fallbackItem = new FallbackExecuteItem(_settingsManager, _shellListPage.AddToHistory);
_shellPageItem = new CommandItem(_shellListPage)
{
Icon = Icons.RunV2Icon,
Title = Resources.shell_command_name,
@@ -39,4 +47,6 @@ public partial class ShellCommandsProvider : CommandProvider
public override ICommandItem[] TopLevelCommands() => [_shellPageItem];
public override IFallbackCommandItem[]? FallbackCommands() => [_fallbackItem];
public static bool SuppressFileFallbackIf(string query) => FallbackExecuteItem.SuppressFileFallbackIf(query);
}

View File

@@ -15,21 +15,26 @@ internal sealed partial class FallbackExecuteSearchItem : FallbackCommandItem
{
private readonly SearchWebCommand _executeItem;
private static readonly CompositeFormat PluginOpen = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open);
private static readonly CompositeFormat SubtitleText = System.Text.CompositeFormat.Parse(Properties.Resources.web_search_fallback_subtitle);
private string _title;
public FallbackExecuteSearchItem(SettingsManager settings)
: base(new SearchWebCommand(string.Empty, settings) { Id = "com.microsoft.websearch.fallback" }, Resources.command_item_title)
{
_executeItem = (SearchWebCommand)this.Command!;
Title = string.Empty;
Subtitle = string.Empty;
_executeItem.Name = string.Empty;
Subtitle = string.Format(CultureInfo.CurrentCulture, PluginOpen, BrowserInfo.Name ?? BrowserInfo.MSEdgeName);
_title = string.Format(CultureInfo.CurrentCulture, PluginOpen, BrowserInfo.Name ?? BrowserInfo.MSEdgeName);
Icon = Icons.WebSearch;
}
public override void UpdateQuery(string query)
{
_executeItem.Arguments = query;
_executeItem.Name = string.IsNullOrEmpty(query) ? string.Empty : Properties.Resources.open_in_default_browser;
Title = query;
var isEmpty = string.IsNullOrEmpty(query);
_executeItem.Name = isEmpty ? string.Empty : Properties.Resources.open_in_default_browser;
Title = isEmpty ? string.Empty : _title;
Subtitle = string.Format(CultureInfo.CurrentCulture, SubtitleText, query);
}
}

View File

@@ -248,5 +248,14 @@ namespace Microsoft.CmdPal.Ext.WebSearch.Properties {
return ResourceManager.GetString("settings_page_name", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Search for &quot;{0}&quot;.
/// </summary>
public static string web_search_fallback_subtitle {
get {
return ResourceManager.GetString("web_search_fallback_subtitle", resourceCulture);
}
}
}
}

View File

@@ -178,6 +178,9 @@
<data name="settings_page_name" xml:space="preserve">
<value>Settings</value>
</data>
<data name="web_search_fallback_subtitle" xml:space="preserve">
<value>Search for "{0}"</value>
</data>
<data name="open_url_fallback_title" xml:space="preserve">
<value>Open URL</value>
</data>

View File

@@ -43,7 +43,6 @@ public partial class ListHelpers
}
public static IEnumerable<T> FilterList<T>(IEnumerable<T> items, string query, Func<string, T, int> scoreFunction)
where T : class
{
var scores = items
.Select(li => new Scored<T>() { Item = li, Score = scoreFunction(query, li) })

View File

@@ -4,7 +4,7 @@
namespace Microsoft.CommandPalette.Extensions.Toolkit;
public sealed partial class OpenUrlCommand : InvokableCommand
public partial class OpenUrlCommand : InvokableCommand
{
private readonly string _target;