mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-29 16:36:40 +01:00
Compare commits
88 Commits
dev/vanzue
...
dev/migrie
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
752ec2deb0 | ||
|
|
d9c168c3fe | ||
|
|
210febe270 | ||
|
|
e53cb409b4 | ||
|
|
98c771788b | ||
|
|
5dee7c9d89 | ||
|
|
c2ba9af144 | ||
|
|
1368f62aed | ||
|
|
1cf28270c2 | ||
|
|
0562415044 | ||
|
|
cfcaafca03 | ||
|
|
a748f653ac | ||
|
|
b56309efbc | ||
|
|
dc439540c3 | ||
|
|
f403e97a0d | ||
|
|
01a6cec411 | ||
|
|
1fafcb06dc | ||
|
|
29c15601f0 | ||
|
|
558e2af6cb | ||
|
|
70c5e3c9d5 | ||
|
|
986ffbeede | ||
|
|
8466c47059 | ||
|
|
229d3b4991 | ||
|
|
7ca9798df5 | ||
|
|
a6e39d5535 | ||
|
|
edf02497af | ||
|
|
02b583267d | ||
|
|
e2569ec4ee | ||
|
|
c760962573 | ||
|
|
ec2480385b | ||
|
|
458d3a2699 | ||
|
|
a58b802cb9 | ||
|
|
a72bf5aed7 | ||
|
|
fb49f6a5e5 | ||
|
|
b4a7bb4a7a | ||
|
|
8615c48c5c | ||
|
|
1aa78e1b96 | ||
|
|
b0c862dd67 | ||
|
|
b6e3b8a3ee | ||
|
|
a2d0d3b262 | ||
|
|
ee53a6d138 | ||
|
|
126a3c0de8 | ||
|
|
a94bd91dba | ||
|
|
b2f2462ad6 | ||
|
|
b6f0ced53e | ||
|
|
a405f27d19 | ||
|
|
86d04cc3bd | ||
|
|
381482e9a0 | ||
|
|
9e7d212c31 | ||
|
|
11c9d913cc | ||
|
|
31f5af7e14 | ||
|
|
568c2ca388 | ||
|
|
27124972cd | ||
|
|
ca9488c875 | ||
|
|
68461ab553 | ||
|
|
ce150322ed | ||
|
|
d73a8a0a2c | ||
|
|
c953ce7eca | ||
|
|
9fc7a180d4 | ||
|
|
ccca007562 | ||
|
|
07c85065f2 | ||
|
|
c06767b73e | ||
|
|
4c639a085c | ||
|
|
8aa7349f32 | ||
|
|
fa86f2223c | ||
|
|
30215e8b4f | ||
|
|
ee386e7be4 | ||
|
|
9ba2030b5e | ||
|
|
1ba832b732 | ||
|
|
46f27c1612 | ||
|
|
1d81ef8935 | ||
|
|
e19cdba074 | ||
|
|
4223286061 | ||
|
|
8e6bd141ca | ||
|
|
d29121c3fd | ||
|
|
58e0530980 | ||
|
|
9272e3112b | ||
|
|
a64095c3d3 | ||
|
|
49480041cd | ||
|
|
547b664a8c | ||
|
|
72320bea79 | ||
|
|
910de53a0a | ||
|
|
11f60de543 | ||
|
|
e7eb2d0239 | ||
|
|
aefae2935e | ||
|
|
727367960e | ||
|
|
32700658fd | ||
|
|
a7cb535515 |
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
// };
|
||||
}
|
||||
|
||||
@@ -222,7 +222,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();
|
||||
|
||||
@@ -232,8 +232,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
|
||||
|
||||
@@ -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;
|
||||
@@ -158,10 +160,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
}
|
||||
|
||||
// Cancel any ongoing search
|
||||
if (_cancellationTokenSource != null)
|
||||
{
|
||||
_cancellationTokenSource.Cancel();
|
||||
}
|
||||
_cancellationTokenSource?.Cancel();
|
||||
|
||||
lock (_listLock)
|
||||
{
|
||||
@@ -373,6 +372,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
}
|
||||
|
||||
TextToSuggest = item.TextToSuggest;
|
||||
WeakReferenceMessenger.Default.Send<UpdateSuggestionMessage>(new(item.TextToSuggest));
|
||||
});
|
||||
|
||||
_lastSelectedItem = item;
|
||||
@@ -426,6 +426,8 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
|
||||
WeakReferenceMessenger.Default.Send<HideDetailsMessage>();
|
||||
|
||||
WeakReferenceMessenger.Default.Send<UpdateSuggestionMessage>(new(string.Empty));
|
||||
|
||||
TextToSuggest = string.Empty;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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.UI.ViewModels.Messages;
|
||||
|
||||
public record UpdateSuggestionMessage(string TextToSuggest)
|
||||
{
|
||||
}
|
||||
@@ -8,6 +8,7 @@ using Microsoft.CmdPal.Common.Services;
|
||||
using Microsoft.CmdPal.Ext.Apps;
|
||||
using Microsoft.CmdPal.Ext.Bookmarks;
|
||||
using Microsoft.CmdPal.Ext.Calc;
|
||||
using Microsoft.CmdPal.Ext.ClipboardHistory;
|
||||
using Microsoft.CmdPal.Ext.Indexer;
|
||||
using Microsoft.CmdPal.Ext.Registry;
|
||||
using Microsoft.CmdPal.Ext.Shell;
|
||||
@@ -96,14 +97,18 @@ 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>();
|
||||
services.AddSingleton<ICommandProvider, WebSearchCommandsProvider>();
|
||||
services.AddSingleton<ICommandProvider, ClipboardHistoryCommandsProvider>();
|
||||
|
||||
// GH #38440: Users might not have WinGet installed! Or they might have
|
||||
// a ridiculously old version. Or might be running as admin.
|
||||
@@ -141,6 +146,7 @@ public partial class App : Application
|
||||
services.AddSingleton(state);
|
||||
services.AddSingleton<IExtensionService, ExtensionService>();
|
||||
services.AddSingleton<TrayIconService>();
|
||||
services.AddSingleton<IRunHistoryService, RunHistoryService>();
|
||||
|
||||
// ViewModels
|
||||
services.AddSingleton<ShellViewModel>();
|
||||
|
||||
@@ -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.UI.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))
|
||||
@@ -204,12 +200,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)
|
||||
@@ -223,7 +272,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
|
||||
@@ -232,23 +281,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/
|
||||
@@ -258,6 +306,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)
|
||||
{
|
||||
@@ -277,6 +340,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))
|
||||
{
|
||||
@@ -294,4 +359,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);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,6 +264,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)
|
||||
|
||||
@@ -107,6 +107,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" />
|
||||
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.ClipboardHistory\Microsoft.CmdPal.Ext.ClipboardHistory.csproj" />
|
||||
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.System\Microsoft.CmdPal.Ext.System.csproj" />
|
||||
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.WebSearch\Microsoft.CmdPal.Ext.WebSearch.csproj" />
|
||||
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Indexer\Microsoft.CmdPal.Ext.Indexer.csproj" />
|
||||
|
||||
50
src/modules/cmdpal/Microsoft.CmdPal.UI/RunHistoryService.cs
Normal file
50
src/modules/cmdpal/Microsoft.CmdPal.UI/RunHistoryService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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}" />
|
||||
|
||||
@@ -383,4 +383,5 @@ namespace winrt::Microsoft::Terminal::UI::implementation
|
||||
icon.Height(targetSize);
|
||||
return icon;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
87
src/modules/cmdpal/Microsoft.Terminal.UI/RunHistory.cpp
Normal file
87
src/modules/cmdpal/Microsoft.Terminal.UI/RunHistory.cpp
Normal 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 hmru = _createMRUList(&mi))
|
||||
{
|
||||
auto freeMRUList = wil::scope_exit([=]() {
|
||||
_freeMRUList(hmru);
|
||||
});
|
||||
|
||||
for (int nMax = _enumMRUList(hmru, -1, NULL, 0), i = 0; i < nMax; ++i)
|
||||
{
|
||||
WCHAR szCommand[MAX_PATH + 2];
|
||||
|
||||
const auto length = _enumMRUList(hmru, i, szCommand, ARRAYSIZE(szCommand));
|
||||
if (length > 1)
|
||||
{
|
||||
// clip off the null-terminator
|
||||
std::wstring_view text{ szCommand, wil::safe_cast<size_t>(length - 1) };
|
||||
//#pragma disable warning(C26493)
|
||||
#pragma warning( push )
|
||||
#pragma warning( disable : 26493 )
|
||||
if (text.back() == L'\\')
|
||||
{
|
||||
// old MRU format has a slash at the end with the show cmd
|
||||
text = { szCommand, wil::safe_cast<size_t>(length - 2) };
|
||||
#pragma warning( pop )
|
||||
if (text.empty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
history.emplace_back(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update dropdown & initial value
|
||||
return winrt::single_threaded_observable_vector<winrt::hstring>(std::move(history));
|
||||
}
|
||||
}
|
||||
21
src/modules/cmdpal/Microsoft.Terminal.UI/RunHistory.h
Normal file
21
src/modules/cmdpal/Microsoft.Terminal.UI/RunHistory.h
Normal 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);
|
||||
}
|
||||
11
src/modules/cmdpal/Microsoft.Terminal.UI/RunHistory.idl
Normal file
11
src/modules/cmdpal/Microsoft.Terminal.UI/RunHistory.idl
Normal 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();
|
||||
};
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
23
src/modules/cmdpal/Microsoft.Terminal.UI/types.h
Normal file
23
src/modules/cmdpal/Microsoft.Terminal.UI/types.h
Normal 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;
|
||||
};
|
||||
@@ -2,8 +2,6 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.CmdPal.Ext.Bookmarks.Properties;
|
||||
@@ -75,31 +73,9 @@ internal sealed partial class AddBookmarkForm : FormContent
|
||||
var formBookmark = formInput["bookmark"] ?? string.Empty;
|
||||
var hasPlaceholder = formBookmark.ToString().Contains('{') && formBookmark.ToString().Contains('}');
|
||||
|
||||
// Determine the type of the bookmark
|
||||
string bookmarkType;
|
||||
|
||||
if (formBookmark.ToString().StartsWith("http://", StringComparison.OrdinalIgnoreCase) || formBookmark.ToString().StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
bookmarkType = "web";
|
||||
}
|
||||
else if (File.Exists(formBookmark.ToString()))
|
||||
{
|
||||
bookmarkType = "file";
|
||||
}
|
||||
else if (Directory.Exists(formBookmark.ToString()))
|
||||
{
|
||||
bookmarkType = "folder";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Default to web if we can't determine the type
|
||||
bookmarkType = "web";
|
||||
}
|
||||
|
||||
var updated = _bookmark ?? new BookmarkData();
|
||||
updated.Name = formName.ToString();
|
||||
updated.Bookmark = formBookmark.ToString();
|
||||
updated.Type = bookmarkType;
|
||||
|
||||
AddedCommand?.Invoke(this, updated);
|
||||
return CommandResult.GoHome();
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Bookmarks;
|
||||
|
||||
@@ -12,8 +14,38 @@ public class BookmarkData
|
||||
|
||||
public string Bookmark { get; set; } = string.Empty;
|
||||
|
||||
public string Type { get; set; } = string.Empty;
|
||||
|
||||
// public string Type { get; set; } = string.Empty;
|
||||
[JsonIgnore]
|
||||
public bool IsPlaceholder => Bookmark.Contains('{') && Bookmark.Contains('}');
|
||||
|
||||
internal void GetExeAndArgs(out string exe, out string args)
|
||||
{
|
||||
ShellHelpers.ParseExecutableAndArgs(Bookmark, out exe, out args);
|
||||
}
|
||||
|
||||
internal bool IsWebUrl()
|
||||
{
|
||||
GetExeAndArgs(out var exe, out var args);
|
||||
if (string.IsNullOrEmpty(exe))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Uri.TryCreate(exe, UriKind.Absolute, out var uri))
|
||||
{
|
||||
if (uri.Scheme == Uri.UriSchemeFile)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// return true if the scheme is http or https, or if there's no scheme (e.g., "www.example.com") but there is a dot in the host
|
||||
return
|
||||
uri.Scheme == Uri.UriSchemeHttp ||
|
||||
uri.Scheme == Uri.UriSchemeHttps ||
|
||||
(string.IsNullOrEmpty(uri.Scheme) && uri.Host.Contains('.'));
|
||||
}
|
||||
|
||||
// If we can't parse it as a URI, we assume it's not a web URL
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,14 @@
|
||||
// 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.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.RegularExpressions;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Ext.Bookmarks.Properties;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.System;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Bookmarks;
|
||||
|
||||
@@ -25,7 +22,7 @@ internal sealed partial class BookmarkPlaceholderForm : FormContent
|
||||
private readonly string _bookmark = string.Empty;
|
||||
|
||||
// TODO pass in an array of placeholders
|
||||
public BookmarkPlaceholderForm(string name, string url, string type)
|
||||
public BookmarkPlaceholderForm(string name, string url)
|
||||
{
|
||||
_bookmark = url;
|
||||
var r = new Regex(Regex.Escape("{") + "(.*?)" + Regex.Escape("}"));
|
||||
@@ -88,23 +85,8 @@ internal sealed partial class BookmarkPlaceholderForm : FormContent
|
||||
target = target.Replace(placeholderString, placeholderData);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var uri = UrlCommand.GetUri(target);
|
||||
if (uri != null)
|
||||
{
|
||||
_ = Launcher.LaunchUriAsync(uri);
|
||||
}
|
||||
else
|
||||
{
|
||||
// throw new UriFormatException("The provided URL is not valid.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex.Message);
|
||||
}
|
||||
var success = UrlCommand.LaunchCommand(target);
|
||||
|
||||
return CommandResult.GoHome();
|
||||
return success ? CommandResult.Dismiss() : CommandResult.KeepOpen();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
@@ -9,19 +10,30 @@ namespace Microsoft.CmdPal.Ext.Bookmarks;
|
||||
|
||||
internal sealed partial class BookmarkPlaceholderPage : ContentPage
|
||||
{
|
||||
private readonly Lazy<IconInfo> _icon;
|
||||
private readonly FormContent _bookmarkPlaceholder;
|
||||
|
||||
public override IContent[] GetContent() => [_bookmarkPlaceholder];
|
||||
|
||||
public override IconInfo Icon { get => _icon.Value; set => base.Icon = value; }
|
||||
|
||||
public BookmarkPlaceholderPage(BookmarkData data)
|
||||
: this(data.Name, data.Bookmark, data.Type)
|
||||
: this(data.Name, data.Bookmark)
|
||||
{
|
||||
}
|
||||
|
||||
public BookmarkPlaceholderPage(string name, string url, string type)
|
||||
public BookmarkPlaceholderPage(string name, string url)
|
||||
{
|
||||
Name = name;
|
||||
Icon = new IconInfo(UrlCommand.IconFromUrl(url, type));
|
||||
_bookmarkPlaceholder = new BookmarkPlaceholderForm(name, url, type);
|
||||
Name = Properties.Resources.bookmarks_command_name_open;
|
||||
|
||||
_bookmarkPlaceholder = new BookmarkPlaceholderForm(name, url);
|
||||
|
||||
_icon = new Lazy<IconInfo>(() =>
|
||||
{
|
||||
ShellHelpers.ParseExecutableAndArgs(url, out var exe, out var args);
|
||||
var t = UrlCommand.GetIconForPath(exe);
|
||||
t.Wait();
|
||||
return t.Result;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using ManagedCommon;
|
||||
@@ -39,10 +38,7 @@ public partial class BookmarksCommandProvider : CommandProvider
|
||||
private void AddNewCommand_AddedCommand(object sender, BookmarkData args)
|
||||
{
|
||||
ExtensionHost.LogMessage($"Adding bookmark ({args.Name},{args.Bookmark})");
|
||||
if (_bookmarks != null)
|
||||
{
|
||||
_bookmarks.Data.Add(args);
|
||||
}
|
||||
_bookmarks?.Data.Add(args);
|
||||
|
||||
SaveAndUpdateCommands();
|
||||
}
|
||||
@@ -120,7 +116,7 @@ public partial class BookmarksCommandProvider : CommandProvider
|
||||
// Add commands for folder types
|
||||
if (command is UrlCommand urlCommand)
|
||||
{
|
||||
if (urlCommand.Type == "folder")
|
||||
if (!bookmark.IsWebUrl())
|
||||
{
|
||||
contextMenu.Add(
|
||||
new CommandContextItem(new DirectoryPage(urlCommand.Url)));
|
||||
@@ -128,10 +124,11 @@ public partial class BookmarksCommandProvider : CommandProvider
|
||||
contextMenu.Add(
|
||||
new CommandContextItem(new OpenInTerminalCommand(urlCommand.Url)));
|
||||
}
|
||||
|
||||
listItem.Subtitle = urlCommand.Url;
|
||||
}
|
||||
|
||||
listItem.Title = bookmark.Name;
|
||||
listItem.Subtitle = bookmark.Bookmark;
|
||||
|
||||
var edit = new AddBookmarkPage(bookmark) { Icon = EditIcon };
|
||||
edit.AddedCommand += Edit_AddedCommand;
|
||||
contextMenu.Add(new CommandContextItem(edit));
|
||||
|
||||
@@ -78,6 +78,15 @@ namespace Microsoft.CmdPal.Ext.Bookmarks.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Open.
|
||||
/// </summary>
|
||||
public static string bookmarks_command_name_open {
|
||||
get {
|
||||
return ResourceManager.GetString("bookmarks_command_name_open", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Delete.
|
||||
/// </summary>
|
||||
|
||||
@@ -148,6 +148,9 @@
|
||||
<data name="bookmarks_form_open" xml:space="preserve">
|
||||
<value>Open</value>
|
||||
</data>
|
||||
<data name="bookmarks_command_name_open" xml:space="preserve">
|
||||
<value>Open</value>
|
||||
</data>
|
||||
<data name="bookmarks_form_name_required" xml:space="preserve">
|
||||
<value>Name is required</value>
|
||||
</data>
|
||||
|
||||
@@ -3,52 +3,89 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.Storage.Streams;
|
||||
using Windows.System;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Bookmarks;
|
||||
|
||||
public partial class UrlCommand : InvokableCommand
|
||||
{
|
||||
public string Type { get; }
|
||||
private readonly Lazy<IconInfo> _icon;
|
||||
|
||||
public string Url { get; }
|
||||
|
||||
public override IconInfo Icon { get => _icon.Value; set => base.Icon = value; }
|
||||
|
||||
public UrlCommand(BookmarkData data)
|
||||
: this(data.Name, data.Bookmark, data.Type)
|
||||
: this(data.Name, data.Bookmark)
|
||||
{
|
||||
}
|
||||
|
||||
public UrlCommand(string name, string url, string type)
|
||||
public UrlCommand(string name, string url)
|
||||
{
|
||||
Name = name;
|
||||
Type = type;
|
||||
Name = Properties.Resources.bookmarks_command_name_open;
|
||||
|
||||
Url = url;
|
||||
Icon = new IconInfo(IconFromUrl(Url, type));
|
||||
|
||||
_icon = new Lazy<IconInfo>(() =>
|
||||
{
|
||||
ShellHelpers.ParseExecutableAndArgs(Url, out var exe, out var args);
|
||||
var t = GetIconForPath(exe);
|
||||
t.Wait();
|
||||
return t.Result;
|
||||
});
|
||||
}
|
||||
|
||||
public override CommandResult Invoke()
|
||||
{
|
||||
var target = Url;
|
||||
try
|
||||
var success = LaunchCommand(Url);
|
||||
|
||||
return success ? CommandResult.Dismiss() : CommandResult.KeepOpen();
|
||||
}
|
||||
|
||||
internal static bool LaunchCommand(string target)
|
||||
{
|
||||
ShellHelpers.ParseExecutableAndArgs(target, out var exe, out var args);
|
||||
return LaunchCommand(exe, args);
|
||||
}
|
||||
|
||||
internal static bool LaunchCommand(string exe, string args)
|
||||
{
|
||||
if (string.IsNullOrEmpty(exe))
|
||||
{
|
||||
var uri = GetUri(target);
|
||||
var message = "No executable found in the command.";
|
||||
Logger.LogError(message);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ShellHelpers.OpenInShell(exe, args))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// If we reach here, it means the command could not be executed
|
||||
// If there aren't args, then try again as a https: uri
|
||||
if (string.IsNullOrEmpty(args))
|
||||
{
|
||||
var uri = GetUri(exe);
|
||||
if (uri != null)
|
||||
{
|
||||
_ = Launcher.LaunchUriAsync(uri);
|
||||
}
|
||||
else
|
||||
{
|
||||
// throw new UriFormatException("The provided URL is not valid.");
|
||||
Logger.LogError("The provided URL is not valid.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex.Message);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return CommandResult.Dismiss();
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static Uri? GetUri(string url)
|
||||
@@ -65,35 +102,90 @@ public partial class UrlCommand : InvokableCommand
|
||||
return uri;
|
||||
}
|
||||
|
||||
internal static string IconFromUrl(string url, string type)
|
||||
public static async Task<IconInfo> GetIconForPath(string target)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case "file":
|
||||
return "📄";
|
||||
case "folder":
|
||||
return "📁";
|
||||
case "web":
|
||||
default:
|
||||
// Get the base url up to the first placeholder
|
||||
var placeholderIndex = url.IndexOf('{');
|
||||
var baseString = placeholderIndex > 0 ? url.Substring(0, placeholderIndex) : url;
|
||||
try
|
||||
{
|
||||
var uri = GetUri(baseString);
|
||||
if (uri != null)
|
||||
{
|
||||
var hostname = uri.Host;
|
||||
var faviconUrl = $"{uri.Scheme}://{hostname}/favicon.ico";
|
||||
return faviconUrl;
|
||||
}
|
||||
}
|
||||
catch (UriFormatException ex)
|
||||
{
|
||||
Logger.LogError(ex.Message);
|
||||
}
|
||||
IconInfo? icon = null;
|
||||
|
||||
return "🔗";
|
||||
// First, try to get the icon from the thumbnail helper
|
||||
// This works for local files and folders
|
||||
icon = await MaybeGetIconForPath(target);
|
||||
if (icon != null)
|
||||
{
|
||||
return icon;
|
||||
}
|
||||
|
||||
// Okay, that failed. Try to resolve the full path of the executable
|
||||
var exeExists = 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 = ShellHelpers.FileExistInPath(target, out fullExePath);
|
||||
},
|
||||
CancellationToken.None);
|
||||
|
||||
// Wait for either completion or timeout
|
||||
pathResolutionTask.Wait(cts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Debug.WriteLine("Operation was canceled.");
|
||||
}
|
||||
|
||||
if (exeExists)
|
||||
{
|
||||
// If the executable exists, try to get the icon from the file
|
||||
icon = await MaybeGetIconForPath(fullExePath);
|
||||
if (icon != null)
|
||||
{
|
||||
return icon;
|
||||
}
|
||||
}
|
||||
|
||||
// Get the base url up to the first placeholder
|
||||
var placeholderIndex = target.IndexOf('{');
|
||||
var baseString = placeholderIndex > 0 ? target.Substring(0, placeholderIndex) : target;
|
||||
try
|
||||
{
|
||||
var uri = GetUri(baseString);
|
||||
if (uri != null)
|
||||
{
|
||||
var hostname = uri.Host;
|
||||
var faviconUrl = $"{uri.Scheme}://{hostname}/favicon.ico";
|
||||
icon = new IconInfo(faviconUrl);
|
||||
}
|
||||
}
|
||||
catch (UriFormatException)
|
||||
{
|
||||
}
|
||||
|
||||
// If we still don't have an icon, use the target as the icon
|
||||
icon = icon ?? new IconInfo(target);
|
||||
|
||||
return icon;
|
||||
}
|
||||
|
||||
private static async Task<IconInfo?> MaybeGetIconForPath(string target)
|
||||
{
|
||||
try
|
||||
{
|
||||
var stream = await ThumbnailHelper.GetThumbnail(target);
|
||||
if (stream != null)
|
||||
{
|
||||
var data = new IconData(RandomAccessStreamReference.CreateFromStream(stream));
|
||||
return new IconInfo(data, data);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = CalculatorIcons.ResultIcon,
|
||||
Title = result,
|
||||
Subtitle = query,
|
||||
TextToSuggest = result,
|
||||
MoreCommands = [
|
||||
new CommandContextItem(copyCommandItem.Command)
|
||||
{
|
||||
|
||||
@@ -16,12 +16,13 @@ public partial class ClipboardHistoryCommandsProvider : CommandProvider
|
||||
{
|
||||
_clipboardHistoryListItem = new ListItem(new ClipboardHistoryListPage())
|
||||
{
|
||||
Title = "Search Clipboard History",
|
||||
Icon = new IconInfo("\xE8C8"), // Copy icon
|
||||
Title = Properties.Resources.list_item_title,
|
||||
Subtitle = Properties.Resources.list_item_subtitle,
|
||||
Icon = Icons.ClipboardList,
|
||||
};
|
||||
|
||||
DisplayName = $"Clipboard History";
|
||||
Icon = new IconInfo("\xE8C8"); // Copy icon
|
||||
DisplayName = Properties.Resources.provider_display_name;
|
||||
Icon = Icons.ClipboardList;
|
||||
Id = "Windows.ClipboardHistory";
|
||||
}
|
||||
|
||||
|
||||
@@ -16,20 +16,20 @@ internal sealed partial class CopyCommand : InvokableCommand
|
||||
{
|
||||
_clipboardItem = clipboardItem;
|
||||
_clipboardFormat = clipboardFormat;
|
||||
Name = "Copy";
|
||||
Name = Properties.Resources.copy_command_name;
|
||||
if (clipboardFormat == ClipboardFormat.Text)
|
||||
{
|
||||
Icon = new("\xE8C8"); // Copy icon
|
||||
Icon = Icons.Copy;
|
||||
}
|
||||
else
|
||||
{
|
||||
Icon = new("\xE8B9"); // Picture icon
|
||||
Icon = Icons.Picture;
|
||||
}
|
||||
}
|
||||
|
||||
public override CommandResult Invoke()
|
||||
{
|
||||
ClipboardHelper.SetClipboardContent(_clipboardItem, _clipboardFormat);
|
||||
return CommandResult.ShowToast("Copied to clipboard");
|
||||
return CommandResult.ShowToast(Properties.Resources.copied_toast_text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.Common.Messages;
|
||||
using Microsoft.CmdPal.Ext.ClipboardHistory.Models;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Commands;
|
||||
@@ -20,8 +19,8 @@ internal sealed partial class PasteCommand : InvokableCommand
|
||||
{
|
||||
_clipboardItem = clipboardItem;
|
||||
_clipboardFormat = clipboardFormat;
|
||||
Name = "Paste";
|
||||
Icon = new("\xE8C8"); // Copy icon
|
||||
Name = Properties.Resources.paste_command_name;
|
||||
Icon = Icons.Paste;
|
||||
}
|
||||
|
||||
private void HideWindow()
|
||||
@@ -37,8 +36,10 @@ internal sealed partial class PasteCommand : InvokableCommand
|
||||
{
|
||||
ClipboardHelper.SetClipboardContent(_clipboardItem, _clipboardFormat);
|
||||
HideWindow();
|
||||
|
||||
ClipboardHelper.SendPasteKeyCombination();
|
||||
|
||||
Clipboard.DeleteItemFromHistory(_clipboardItem.Item);
|
||||
return CommandResult.ShowToast("Pasting");
|
||||
return CommandResult.ShowToast(Properties.Resources.paste_toast_text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,10 +59,10 @@ internal static class ClipboardHelper
|
||||
output.SetText(text);
|
||||
try
|
||||
{
|
||||
// Clipboard.SetContentWithOptions(output, null);
|
||||
ClipboardThreadQueue.EnqueueTask(() =>
|
||||
{
|
||||
Clipboard.SetContent(output);
|
||||
|
||||
Flush();
|
||||
ExtensionHost.LogMessage(new LogMessage() { Message = "Copied text to clipboard" });
|
||||
});
|
||||
@@ -87,7 +87,7 @@ internal static class ClipboardHelper
|
||||
{
|
||||
try
|
||||
{
|
||||
Task.Run(Clipboard.Flush).Wait();
|
||||
Clipboard.Flush();
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
// 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.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.ClipboardHistory;
|
||||
|
||||
internal sealed class Icons
|
||||
{
|
||||
internal static IconInfo Copy { get; } = new("\xE8C8");
|
||||
|
||||
internal static IconInfo Picture { get; } = new("\xE8B9");
|
||||
|
||||
internal static IconInfo Paste { get; } = new("\uE77F");
|
||||
|
||||
internal static IconInfo ClipboardList { get; } = new("\uF0E3");
|
||||
}
|
||||
@@ -15,4 +15,19 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- String resources -->
|
||||
<ItemGroup>
|
||||
<Compile Update="Properties\Resources.Designer.cs">
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Properties\Resources.resx">
|
||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -24,8 +24,8 @@ internal sealed partial class ClipboardHistoryListPage : ListPage
|
||||
{
|
||||
clipboardHistory = [];
|
||||
_defaultIconPath = string.Empty;
|
||||
Icon = new("\uF0E3"); // ClipboardList icon
|
||||
Name = "Clipboard History";
|
||||
Icon = Icons.ClipboardList;
|
||||
Name = Properties.Resources.clipboard_history_page_name;
|
||||
Id = "com.microsoft.cmdpal.clipboardHistory";
|
||||
ShowDetails = true;
|
||||
|
||||
@@ -113,7 +113,7 @@ internal sealed partial class ClipboardHistoryListPage : ListPage
|
||||
{
|
||||
// TODO GH #108 We need to figure out some logging
|
||||
// Logger.LogError("Loading clipboard history failed", ex);
|
||||
ExtensionHost.ShowStatus(new StatusMessage() { Message = "Loading clipboard history failed", State = MessageState.Error }, StatusContext.Page);
|
||||
ExtensionHost.ShowStatus(new StatusMessage() { Message = Properties.Resources.clipboard_failed_to_load, State = MessageState.Error }, StatusContext.Page);
|
||||
ExtensionHost.LogMessage(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
144
src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Properties/Resources.Designer.cs
generated
Normal file
144
src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Properties/Resources.Designer.cs
generated
Normal file
@@ -0,0 +1,144 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version:4.0.30319.42000
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Properties {
|
||||
using System;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||
/// </summary>
|
||||
// This class was auto-generated by the StronglyTypedResourceBuilder
|
||||
// class via a tool like ResGen or Visual Studio.
|
||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||
// with the /str option, or rebuild your VS project.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
public class Resources {
|
||||
|
||||
private static global::System.Resources.ResourceManager resourceMan;
|
||||
|
||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||
|
||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||
internal Resources() {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the cached ResourceManager instance used by this class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
public static global::System.Resources.ResourceManager ResourceManager {
|
||||
get {
|
||||
if (object.ReferenceEquals(resourceMan, null)) {
|
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.CmdPal.Ext.ClipboardHistory.Properties.Resources", typeof(Resources).Assembly);
|
||||
resourceMan = temp;
|
||||
}
|
||||
return resourceMan;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the current thread's CurrentUICulture property for all
|
||||
/// resource lookups using this strongly typed resource class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
public static global::System.Globalization.CultureInfo Culture {
|
||||
get {
|
||||
return resourceCulture;
|
||||
}
|
||||
set {
|
||||
resourceCulture = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Loading clipboard history failed.
|
||||
/// </summary>
|
||||
public static string clipboard_failed_to_load {
|
||||
get {
|
||||
return ResourceManager.GetString("clipboard_failed_to_load", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Open.
|
||||
/// </summary>
|
||||
public static string clipboard_history_page_name {
|
||||
get {
|
||||
return ResourceManager.GetString("clipboard_history_page_name", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Copied to clipboard.
|
||||
/// </summary>
|
||||
public static string copied_toast_text {
|
||||
get {
|
||||
return ResourceManager.GetString("copied_toast_text", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Copy.
|
||||
/// </summary>
|
||||
public static string copy_command_name {
|
||||
get {
|
||||
return ResourceManager.GetString("copy_command_name", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Copy, paste, and search items on the clipboard.
|
||||
/// </summary>
|
||||
public static string list_item_subtitle {
|
||||
get {
|
||||
return ResourceManager.GetString("list_item_subtitle", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Clipboard History.
|
||||
/// </summary>
|
||||
public static string list_item_title {
|
||||
get {
|
||||
return ResourceManager.GetString("list_item_title", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Paste.
|
||||
/// </summary>
|
||||
public static string paste_command_name {
|
||||
get {
|
||||
return ResourceManager.GetString("paste_command_name", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Pasting.
|
||||
/// </summary>
|
||||
public static string paste_toast_text {
|
||||
get {
|
||||
return ResourceManager.GetString("paste_toast_text", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Clipboard History.
|
||||
/// </summary>
|
||||
public static string provider_display_name {
|
||||
get {
|
||||
return ResourceManager.GetString("provider_display_name", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="copy_command_name" xml:space="preserve">
|
||||
<value>Copy</value>
|
||||
</data>
|
||||
<data name="paste_command_name" xml:space="preserve">
|
||||
<value>Paste</value>
|
||||
</data>
|
||||
<data name="paste_toast_text" xml:space="preserve">
|
||||
<value>Pasting</value>
|
||||
</data>
|
||||
<data name="copied_toast_text" xml:space="preserve">
|
||||
<value>Copied to clipboard</value>
|
||||
</data>
|
||||
<data name="list_item_title" xml:space="preserve">
|
||||
<value>Clipboard History</value>
|
||||
</data>
|
||||
<data name="list_item_subtitle" xml:space="preserve">
|
||||
<value>Copy, paste, and search items on the clipboard</value>
|
||||
</data>
|
||||
<data name="provider_display_name" xml:space="preserve">
|
||||
<value>Clipboard History</value>
|
||||
</data>
|
||||
<data name="clipboard_history_page_name" xml:space="preserve">
|
||||
<value>Open</value>
|
||||
</data>
|
||||
<data name="clipboard_failed_to_load" xml:space="preserve">
|
||||
<value>Loading clipboard history failed</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -21,6 +21,8 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, System
|
||||
|
||||
private uint _queryCookie = 10;
|
||||
|
||||
private Func<string, bool> _suppressCallback;
|
||||
|
||||
public FallbackOpenFileItem()
|
||||
: base(new NoOpCommand(), Resources.Indexer_Find_Path_fallback_display_title)
|
||||
{
|
||||
@@ -41,6 +43,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.
|
||||
@@ -125,4 +138,9 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, System
|
||||
_searchEngine.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public void SuppressFallbackWhen(Func<string, bool> callback)
|
||||
{
|
||||
_suppressCallback = callback;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,17 +26,17 @@ internal sealed partial class ExecuteItem : InvokableCommand
|
||||
if (type == RunAsType.Administrator)
|
||||
{
|
||||
Name = Properties.Resources.cmd_run_as_administrator;
|
||||
Icon = new IconInfo("\xE7EF"); // Admin Icon
|
||||
Icon = Icons.RunAsAdmin;
|
||||
}
|
||||
else if (type == RunAsType.OtherUser)
|
||||
{
|
||||
Name = Properties.Resources.cmd_run_as_user;
|
||||
Icon = new IconInfo("\xE7EE"); // User Icon
|
||||
Icon = Icons.RunAsUser;
|
||||
}
|
||||
else
|
||||
{
|
||||
Name = Properties.Resources.generic_run_command;
|
||||
Icon = new IconInfo("\uE751"); // Return Key Icon
|
||||
Icon = Icons.RunV2;
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@@ -2,37 +2,196 @@
|
||||
// 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)
|
||||
: base(new ExecuteItem(string.Empty, settings), Resources.shell_command_display_title)
|
||||
public FallbackExecuteItem(SettingsManager settings, Action<string>? addToHistory)
|
||||
: base(new NoOpCommand(), 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.RunV2; // Defined in Icons.cs and contains the execute command icon.
|
||||
Icon = Icons.RunV2;
|
||||
_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;
|
||||
}
|
||||
|
||||
ShellHelpers.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;
|
||||
}
|
||||
|
||||
ShellHelpers.ParseExecutableAndArgs(searchText, out var exe, out var args);
|
||||
var exeExists = ShellListPageHelpers.FileExistInPath(exe, out var fullExePath);
|
||||
var pathIsDir = Directory.Exists(exe);
|
||||
|
||||
return exeExists || pathIsDir;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,15 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
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 +29,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,
|
||||
@@ -38,7 +41,7 @@ public class ShellListPageHelpers
|
||||
|
||||
private List<ListItem> GetHistoryCmds(string cmd, ListItem result)
|
||||
{
|
||||
IEnumerable<ListItem?> history = _settings.Count.Where(o => o.Key.Contains(cmd, StringComparison.CurrentCultureIgnoreCase))
|
||||
var history = _settings.Count.Where(o => o.Key.Contains(cmd, StringComparison.CurrentCultureIgnoreCase))
|
||||
.OrderByDescending(o => o.Value)
|
||||
.Select(m =>
|
||||
{
|
||||
@@ -66,7 +69,7 @@ public class ShellListPageHelpers
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(query);
|
||||
|
||||
List<ListItem> results = new List<ListItem>();
|
||||
var results = new List<ListItem>();
|
||||
var cmd = query;
|
||||
if (string.IsNullOrEmpty(cmd))
|
||||
{
|
||||
@@ -101,7 +104,7 @@ public class ShellListPageHelpers
|
||||
|
||||
private List<ListItem> ResultsFromHistory()
|
||||
{
|
||||
IEnumerable<ListItem> history = _settings.Count.OrderByDescending(o => o.Value)
|
||||
var history = _settings.Count.OrderByDescending(o => o.Value)
|
||||
.Select(m => new ListItem(new ExecuteItem(m.Key, _settings))
|
||||
{
|
||||
Title = m.Key,
|
||||
@@ -113,4 +116,89 @@ public class ShellListPageHelpers
|
||||
|
||||
return history.ToList();
|
||||
}
|
||||
|
||||
internal static bool FileExistInPath(string filename)
|
||||
{
|
||||
return FileExistInPath(filename, out var _);
|
||||
}
|
||||
|
||||
internal static bool FileExistInPath(string filename, out string fullPath, CancellationToken? token = null)
|
||||
{
|
||||
// TODO! remove this method and just use ShellHelpers.FileExistInPath directly
|
||||
return ShellHelpers.FileExistInPath(filename, out fullPath, token ?? CancellationToken.None);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
ShellHelpers.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)
|
||||
{
|
||||
Debug.WriteLine("Operation was canceled.");
|
||||
}
|
||||
|
||||
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.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;
|
||||
}
|
||||
|
||||
return li;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,4 +9,10 @@ namespace Microsoft.CmdPal.Ext.Shell;
|
||||
internal sealed class Icons
|
||||
{
|
||||
internal static IconInfo RunV2 { get; } = IconHelpers.FromRelativePath("Assets\\Run.svg");
|
||||
|
||||
internal static IconInfo Folder { get; } = new("📁");
|
||||
|
||||
internal static IconInfo RunAsAdmin { get; } = new("\xE7EF"); // admin
|
||||
|
||||
internal static IconInfo RunAsUser { get; } = new("\uE7EE"); // user
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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.RunAsAdmin,
|
||||
}),
|
||||
new CommandContextItem(
|
||||
new AnonymousCommand(RunAsOther)
|
||||
{
|
||||
Name = Properties.Resources.cmd_run_as_user,
|
||||
Icon = Icons.RunAsUser,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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,480 @@ 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.RunV2;
|
||||
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.RunV2,
|
||||
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;
|
||||
}
|
||||
|
||||
ShellHelpers.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 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();
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.Folder : Icons.RunV2;
|
||||
return icon;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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.RunV2;
|
||||
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.RunV2,
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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), 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 = IconHelpers.FromRelativePath("Assets\\WebSearch.png");
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ using System.Globalization;
|
||||
using System.Text;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Commands;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Properties;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo;
|
||||
|
||||
@@ -33,6 +32,7 @@ internal sealed partial class FallbackOpenURLItem : FallbackCommandItem
|
||||
{
|
||||
if (!IsValidUrl(query))
|
||||
{
|
||||
_executeItem.Name = string.Empty;
|
||||
Title = string.Empty;
|
||||
Subtitle = string.Empty;
|
||||
return;
|
||||
|
||||
@@ -239,5 +239,14 @@ namespace Microsoft.CmdPal.Ext.WebSearch.Properties {
|
||||
return ResourceManager.GetString("settings_page_name", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Search for "{0}".
|
||||
/// </summary>
|
||||
public static string web_search_fallback_subtitle {
|
||||
get {
|
||||
return ResourceManager.GetString("web_search_fallback_subtitle", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,4 +178,7 @@
|
||||
<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>
|
||||
</root>
|
||||
@@ -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) })
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public sealed partial class OpenUrlCommand : InvokableCommand
|
||||
public partial class OpenUrlCommand : InvokableCommand
|
||||
{
|
||||
private readonly string _target;
|
||||
|
||||
|
||||
@@ -59,4 +59,101 @@ public static class ShellHelpers
|
||||
Administrator,
|
||||
OtherUser,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses the input string to extract the executable and its arguments.
|
||||
/// </summary>
|
||||
public 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a file exists somewhere in the PATH.
|
||||
/// If it exists, returns the full path to the file in the out parameter.
|
||||
/// If it does not exist, returns false and the out parameter is set to an empty string.
|
||||
/// <param name="filename">The name of the file to check.</param>
|
||||
/// <param name="fullPath">The full path to the file if it exists, otherwise an empty string.</param>
|
||||
/// <param name="token">An optional cancellation token to cancel the operation.</param>
|
||||
/// <returns>True if the file exists in the PATH, otherwise false.</returns>
|
||||
/// </summary>
|
||||
public 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)
|
||||
{
|
||||
foreach (var path in values.Split(';'))
|
||||
{
|
||||
var path1 = Path.Combine(path, filename);
|
||||
if (File.Exists(path1))
|
||||
{
|
||||
fullPath = Path.GetFullPath(path1);
|
||||
return true;
|
||||
}
|
||||
|
||||
token?.ThrowIfCancellationRequested();
|
||||
|
||||
var path2 = Path.Combine(path, filename + ".exe");
|
||||
if (File.Exists(path2))
|
||||
{
|
||||
fullPath = Path.GetFullPath(path2);
|
||||
return true;
|
||||
}
|
||||
|
||||
token?.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user