mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-05 18:57:19 +02:00
Add the Command Palette module (#37908)
Windows Command Palette ("CmdPal") is the next iteration of PowerToys Run. With extensibility at its core, the Command Palette is your one-stop launcher to start _anything_.
By default, CmdPal is bound to <kbd>Win+Alt+Space</kbd>.


----
This brings the current preview version of CmdPal into the upstream PowerToys repo. There are still lots of bugs to work out, but it's reached the state we're ready to start sharing it with the world. From here, we can further collaborate with the community on the features that are important, and ensuring that we've got a most robust API to enable developers to build whatever extensions they want.
Most of the built-in PT Run modules have already been ported to CmdPal's extension API. Those include:
* Installed apps
* Shell commands
* File search (powered by the indexer)
* Windows Registry search
* Web search
* Windows Terminal Profiles
* Windows Services
* Windows settings
There are a couple new extensions built-in
* You can now search for packages on `winget` and install them right from the palette. This also powers searching for extensions for the palette
* The calculator has an entirely new implementation. This is currently less feature complete than the original PT Run one - we're looking forward to updating it to be more complete for future ingestion in Windows
* "Bookmarks" allow you to save shortcuts to files, folders, and webpages as top-level commands in the palette.
We've got a bunch of other samples too, in this repo and elsewhere
### PowerToys specific notes
CmdPal will eventually graduate out of PowerToys to live as its own application, which is why it's implemented just a little differently than most other modules. Enabling CmdPal will install its `msix` package.
The CI was minorly changed to support CmdPal version numbers independent of PowerToys itself. It doesn't make sense for us to start CmdPal at v0.90, and in the future, we want to be able to rev CmdPal independently of PT itself.
Closes #3200, closes #3600, closes #7770, closes #34273, closes #36471, closes #20976, closes #14495
-----
TODOs et al
**Blocking:**
- [ ] Images and descriptions in Settings and OOBE need to be properly defined, as mentioned before
- [ ] Niels is on it
- [x] Doesn't start properly from PowerToys unless the fix PR is merged.
- https://github.com/zadjii-msft/PowerToys/pull/556 merged
- [x] I seem to lose focus a lot when I press on some limits, like between the search bar and the results.
- This is https://github.com/zadjii-msft/PowerToys/issues/427
- [x] Turned off an extension like Calculator and it was still working.
- Need to get rid of that toggle, it doesn't do anything currently
- [x] `ListViewModel.<FetchItems>` crash
- Pretty confident that was fixed in https://github.com/zadjii-msft/PowerToys/pull/553
**Not blocking / improvements:**
- Show the shortcut through settings, as mentioned before, or create a button that would open CmdPalette settings.
- When PowerToys starts, CmdPalette is always shown if enabled. That's weird when just starting PowerToys/ logging in to the computer with PowerToys auto-start activated. I think this should at least be a setting.
- Needing to double press a result for it to do the default action seems quirky. If one is already selected, I think just pressing should be enough for it to do the action.
- This is currently a setting, though we're thinking of changing the setting even more: https://github.com/zadjii-msft/PowerToys/issues/392
- There's no URI extension. Was surprised when typing a URL that it only proposed a web search.
- [x] There's no System commands extension. Was expecting to be able to quickly restart the computer by typing restart but it wasn't there.
- This is in PR https://github.com/zadjii-msft/PowerToys/pull/452
---------
Co-authored-by: joadoumie <98557455+joadoumie@users.noreply.github.com>
Co-authored-by: Jordi Adoumie <jordiadoumie@microsoft.com>
Co-authored-by: Mike Griese <zadjii@gmail.com>
Co-authored-by: Niels Laute <niels.laute@live.nl>
Co-authored-by: Michael Hawker <24302614+michael-hawker@users.noreply.github.com>
Co-authored-by: Stefan Markovic <57057282+stefansjfw@users.noreply.github.com>
Co-authored-by: Seraphima <zykovas91@gmail.com>
Co-authored-by: Jaime Bernardo <jaime@janeasystems.com>
Co-authored-by: Kristen Schau <47155823+krschau@users.noreply.github.com>
Co-authored-by: Eric Johnson <ericjohnson327@gmail.com>
Co-authored-by: Ethan Fang <ethanfang@microsoft.com>
Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com>
Co-authored-by: Clint Rutkas <clint@rutkas.com>
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
// 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.UI.ViewModels.Messages;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.BuiltinCommands;
|
||||
|
||||
/// <summary>
|
||||
/// Built-in Provider for a top-level command which can quit the application. Invokes the <see cref="QuitCommand"/>, which sends a <see cref="QuitMessage"/>.
|
||||
/// </summary>
|
||||
public partial class BuiltInsCommandProvider : CommandProvider
|
||||
{
|
||||
private readonly OpenSettingsCommand openSettings = new();
|
||||
private readonly QuitCommand quitCommand = new();
|
||||
private readonly FallbackReloadItem _fallbackReloadItem = new();
|
||||
private readonly FallbackLogItem _fallbackLogItem = new();
|
||||
private readonly NewExtensionPage _newExtension = new();
|
||||
|
||||
public override ICommandItem[] TopLevelCommands() =>
|
||||
[
|
||||
new CommandItem(openSettings) { Subtitle = Properties.Resources.builtin_open_settings_subtitle },
|
||||
new CommandItem(_newExtension) { Title = _newExtension.Title, Subtitle = Properties.Resources.builtin_new_extension_subtitle },
|
||||
];
|
||||
|
||||
public override IFallbackCommandItem[] FallbackCommands() =>
|
||||
[
|
||||
new FallbackCommandItem(quitCommand, displayTitle: Properties.Resources.builtin_quit_subtitle) { Subtitle = Properties.Resources.builtin_quit_subtitle },
|
||||
_fallbackReloadItem,
|
||||
_fallbackLogItem,
|
||||
];
|
||||
|
||||
public BuiltInsCommandProvider()
|
||||
{
|
||||
Id = "Core";
|
||||
DisplayName = Properties.Resources.builtin_display_name;
|
||||
Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.scale-200.png");
|
||||
}
|
||||
|
||||
public override void InitializeWithHost(IExtensionHost host) => BuiltinsExtensionHost.Instance.Initialize(host);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// 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.Diagnostics.CodeAnalysis;
|
||||
using System.IO.Compression;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.CmdPal.Common;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.BuiltinCommands;
|
||||
|
||||
public partial class BuiltinsExtensionHost
|
||||
{
|
||||
internal static ExtensionHostInstance Instance { get; } = new();
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
// 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.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.BuiltinCommands;
|
||||
|
||||
internal sealed partial class CreatedExtensionForm : NewExtensionFormBase
|
||||
{
|
||||
public CreatedExtensionForm(string name, string displayName, string path)
|
||||
{
|
||||
TemplateJson = CardTemplate;
|
||||
DataJson = $$"""
|
||||
{
|
||||
"name": {{JsonSerializer.Serialize(name)}},
|
||||
"directory": {{JsonSerializer.Serialize(path)}},
|
||||
"displayName": {{JsonSerializer.Serialize(displayName)}}
|
||||
}
|
||||
""";
|
||||
_name = name;
|
||||
_displayName = displayName;
|
||||
_path = path;
|
||||
}
|
||||
|
||||
public override ICommandResult SubmitForm(string inputs, string data)
|
||||
{
|
||||
JsonObject? dataInput = JsonNode.Parse(data)?.AsObject();
|
||||
if (dataInput == null)
|
||||
{
|
||||
return CommandResult.KeepOpen();
|
||||
}
|
||||
|
||||
string verb = dataInput["x"]?.AsValue()?.ToString() ?? string.Empty;
|
||||
return verb switch
|
||||
{
|
||||
"sln" => OpenSolution(),
|
||||
"dir" => OpenDirectory(),
|
||||
"new" => CreateNew(),
|
||||
_ => CommandResult.KeepOpen(),
|
||||
};
|
||||
}
|
||||
|
||||
private ICommandResult OpenSolution()
|
||||
{
|
||||
string[] parts = [_path, _name, $"{_name}.sln"];
|
||||
string pathToSolution = Path.Combine(parts);
|
||||
ShellHelpers.OpenInShell(pathToSolution);
|
||||
return CommandResult.Hide();
|
||||
}
|
||||
|
||||
private ICommandResult OpenDirectory()
|
||||
{
|
||||
string[] parts = [_path, _name];
|
||||
string pathToDir = Path.Combine(parts);
|
||||
ShellHelpers.OpenInShell(pathToDir);
|
||||
return CommandResult.Hide();
|
||||
}
|
||||
|
||||
private ICommandResult CreateNew()
|
||||
{
|
||||
RaiseFormSubmit(null);
|
||||
return CommandResult.KeepOpen();
|
||||
}
|
||||
|
||||
private static readonly string CardTemplate = $$"""
|
||||
{
|
||||
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
"type": "AdaptiveCard",
|
||||
"version": "1.6",
|
||||
"body": [
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "{{Properties.Resources.builtin_create_extension_success}}",
|
||||
"size": "large",
|
||||
"weight": "bolder",
|
||||
"style": "heading",
|
||||
"wrap": true
|
||||
},
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "{{Properties.Resources.builtin_created_in_text}}",
|
||||
"wrap": true
|
||||
},
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "${directory}",
|
||||
"fontType": "monospace"
|
||||
},
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "{{Properties.Resources.builtin_created_next_steps_title}}",
|
||||
"style": "heading",
|
||||
"wrap": true
|
||||
},
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "{{Properties.Resources.builtin_created_next_steps}}",
|
||||
"wrap": true
|
||||
},
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "{{Properties.Resources.builtin_created_next_steps_p2}}",
|
||||
"wrap": true
|
||||
},
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "{{Properties.Resources.builtin_created_next_steps_p3}}",
|
||||
"wrap": true
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": "Action.Submit",
|
||||
"title": "{{Properties.Resources.builtin_create_extension_open_solution}}",
|
||||
"data": {
|
||||
"x": "sln"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Action.Submit",
|
||||
"title": "{{Properties.Resources.builtin_create_extension_open_directory}}",
|
||||
"data": {
|
||||
"x": "dir"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Action.Submit",
|
||||
"title": "{{Properties.Resources.builtin_create_extension_create_another}}",
|
||||
"data": {
|
||||
"x": "new"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
private readonly string _name;
|
||||
private readonly string _displayName;
|
||||
private readonly string _path;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// 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.UI.ViewModels.Commands;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Properties;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.BuiltinCommands;
|
||||
|
||||
internal sealed partial class FallbackLogItem : FallbackCommandItem
|
||||
{
|
||||
private readonly LogMessagesPage _logMessagesPage;
|
||||
|
||||
public FallbackLogItem()
|
||||
: base(new LogMessagesPage(), Resources.builtin_log_subtitle)
|
||||
{
|
||||
_logMessagesPage = (LogMessagesPage)Command!;
|
||||
Title = string.Empty;
|
||||
_logMessagesPage.Name = string.Empty;
|
||||
Subtitle = Properties.Resources.builtin_log_subtitle;
|
||||
}
|
||||
|
||||
public override void UpdateQuery(string query)
|
||||
{
|
||||
_logMessagesPage.Name = query.StartsWith('l') ? Properties.Resources.builtin_log_title : string.Empty;
|
||||
Title = _logMessagesPage.Name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// 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.UI.ViewModels.BuiltinCommands;
|
||||
|
||||
internal sealed partial class FallbackReloadItem : FallbackCommandItem
|
||||
{
|
||||
private readonly ReloadExtensionsCommand _reloadCommand;
|
||||
|
||||
public FallbackReloadItem()
|
||||
: base(new ReloadExtensionsCommand(), Properties.Resources.builtin_reload_display_title)
|
||||
{
|
||||
_reloadCommand = (ReloadExtensionsCommand)Command!;
|
||||
Title = string.Empty;
|
||||
Subtitle = Properties.Resources.builtin_reload_subtitle;
|
||||
}
|
||||
|
||||
public override void UpdateQuery(string query)
|
||||
{
|
||||
_reloadCommand.Name = query.StartsWith('r') ? "Reload" : string.Empty;
|
||||
Title = _reloadCommand.Name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// 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.Specialized;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Commands;
|
||||
|
||||
public partial class LogMessagesPage : ListPage
|
||||
{
|
||||
private readonly List<IListItem> _listItems = new();
|
||||
|
||||
public LogMessagesPage()
|
||||
{
|
||||
Name = Properties.Resources.builtin_log_name;
|
||||
Title = Properties.Resources.builtin_log_page_name;
|
||||
Icon = new IconInfo("\uE8FD"); // BulletedList icon
|
||||
CommandPaletteHost.LogMessages.CollectionChanged += LogMessages_CollectionChanged;
|
||||
}
|
||||
|
||||
private void LogMessages_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems != null)
|
||||
{
|
||||
foreach (var item in e.NewItems)
|
||||
{
|
||||
if (item is LogMessageViewModel logMessageViewModel)
|
||||
{
|
||||
var li = new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = logMessageViewModel.Message,
|
||||
Subtitle = logMessageViewModel.ExtensionPfn,
|
||||
};
|
||||
_listItems.Insert(0, li);
|
||||
}
|
||||
}
|
||||
|
||||
RaiseItemsChanged(_listItems.Count);
|
||||
}
|
||||
}
|
||||
|
||||
public override IListItem[] GetItems()
|
||||
{
|
||||
return _listItems.ToArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
// 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.Specialized;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.Ext.Apps;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.MainPage;
|
||||
|
||||
/// <summary>
|
||||
/// This class encapsulates the data we load from built-in providers and extensions to use within the same extension-UI system for a <see cref="ListPage"/>.
|
||||
/// TODO: Need to think about how we structure/interop for the page -> section -> item between the main setup, the extensions, and our viewmodels.
|
||||
/// </summary>
|
||||
public partial class MainListPage : DynamicListPage,
|
||||
IRecipient<ClearSearchMessage>,
|
||||
IRecipient<UpdateFallbackItemsMessage>
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
private readonly TopLevelCommandManager _tlcManager;
|
||||
private IEnumerable<IListItem>? _filteredItems;
|
||||
|
||||
public MainListPage(IServiceProvider serviceProvider)
|
||||
{
|
||||
Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.scale-200.png");
|
||||
_serviceProvider = serviceProvider;
|
||||
|
||||
_tlcManager = _serviceProvider.GetService<TopLevelCommandManager>()!;
|
||||
_tlcManager.PropertyChanged += TlcManager_PropertyChanged;
|
||||
_tlcManager.TopLevelCommands.CollectionChanged += Commands_CollectionChanged;
|
||||
|
||||
// The all apps page will kick off a BG thread to start loading apps.
|
||||
// We just want to know when it is done.
|
||||
var allApps = AllAppsCommandProvider.Page;
|
||||
allApps.PropChanged += (s, p) =>
|
||||
{
|
||||
if (p.PropertyName == nameof(allApps.IsLoading))
|
||||
{
|
||||
IsLoading = ActuallyLoading();
|
||||
}
|
||||
};
|
||||
|
||||
WeakReferenceMessenger.Default.Register<ClearSearchMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<UpdateFallbackItemsMessage>(this);
|
||||
|
||||
var settings = _serviceProvider.GetService<SettingsModel>()!;
|
||||
settings.SettingsChanged += SettingsChangedHandler;
|
||||
HotReloadSettings(settings);
|
||||
|
||||
IsLoading = true;
|
||||
}
|
||||
|
||||
private void TlcManager_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(IsLoading))
|
||||
{
|
||||
IsLoading = ActuallyLoading();
|
||||
}
|
||||
}
|
||||
|
||||
private void Commands_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) => RaiseItemsChanged(_tlcManager.TopLevelCommands.Count);
|
||||
|
||||
public override IListItem[] GetItems()
|
||||
{
|
||||
if (string.IsNullOrEmpty(SearchText))
|
||||
{
|
||||
lock (_tlcManager.TopLevelCommands)
|
||||
{
|
||||
return _tlcManager
|
||||
.TopLevelCommands
|
||||
.Select(tlc => tlc)
|
||||
.Where(tlc => !string.IsNullOrEmpty(tlc.Title))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
lock (_tlcManager.TopLevelCommands)
|
||||
{
|
||||
return _filteredItems?.ToArray() ?? [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void UpdateSearchText(string oldSearch, string newSearch)
|
||||
{
|
||||
// Handle changes to the filter text here
|
||||
if (!string.IsNullOrEmpty(SearchText))
|
||||
{
|
||||
var aliases = _serviceProvider.GetService<AliasManager>()!;
|
||||
if (aliases.CheckAlias(newSearch))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var commands = _tlcManager.TopLevelCommands;
|
||||
lock (commands)
|
||||
{
|
||||
// This gets called on a background thread, because ListViewModel
|
||||
// updates the .SearchText of all extensions on a BG thread.
|
||||
foreach (var command in commands)
|
||||
{
|
||||
command.TryUpdateFallbackText(newSearch);
|
||||
}
|
||||
|
||||
// Cleared out the filter text? easy. Reset _filteredItems, and bail out.
|
||||
if (string.IsNullOrEmpty(newSearch))
|
||||
{
|
||||
_filteredItems = null;
|
||||
RaiseItemsChanged(commands.Count);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the new string doesn't start with the old string, then we can't
|
||||
// re-use previous results. Reset _filteredItems, and keep er moving.
|
||||
if (!newSearch.StartsWith(oldSearch, StringComparison.CurrentCultureIgnoreCase))
|
||||
{
|
||||
_filteredItems = null;
|
||||
}
|
||||
|
||||
// If we don't have any previous filter results to work with, start
|
||||
// with a list of all our commands & apps.
|
||||
if (_filteredItems == null)
|
||||
{
|
||||
IEnumerable<IListItem> apps = AllAppsCommandProvider.Page.GetItems();
|
||||
_filteredItems = commands.Concat(apps);
|
||||
}
|
||||
|
||||
// Produce a list of everything that matches the current filter.
|
||||
_filteredItems = ListHelpers.FilterList<IListItem>(_filteredItems, SearchText, ScoreTopLevelItem);
|
||||
RaiseItemsChanged(_filteredItems.Count());
|
||||
}
|
||||
}
|
||||
|
||||
private bool ActuallyLoading()
|
||||
{
|
||||
var tlcManager = _serviceProvider.GetService<TopLevelCommandManager>()!;
|
||||
var allApps = AllAppsCommandProvider.Page;
|
||||
return allApps.IsLoading || tlcManager.IsLoading;
|
||||
}
|
||||
|
||||
// Almost verbatim ListHelpers.ScoreListItem, but also accounting for the
|
||||
// fact that we want fallback handlers down-weighted, so that they don't
|
||||
// _always_ show up first.
|
||||
private int ScoreTopLevelItem(string query, IListItem topLevelOrAppItem)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
var title = topLevelOrAppItem.Title;
|
||||
if (string.IsNullOrEmpty(title))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var isFallback = false;
|
||||
var isAliasSubstringMatch = false;
|
||||
var isAliasMatch = false;
|
||||
var id = IdForTopLevelOrAppItem(topLevelOrAppItem);
|
||||
|
||||
var extensionDisplayName = string.Empty;
|
||||
if (topLevelOrAppItem is TopLevelCommandItemWrapper toplevel)
|
||||
{
|
||||
isFallback = toplevel.IsFallback;
|
||||
if (toplevel.Alias?.Alias is string alias)
|
||||
{
|
||||
isAliasMatch = alias == query;
|
||||
isAliasSubstringMatch = isAliasMatch || alias.StartsWith(query, StringComparison.CurrentCultureIgnoreCase);
|
||||
}
|
||||
|
||||
extensionDisplayName = toplevel.ExtensionHost?.Extension?.PackageDisplayName ?? string.Empty;
|
||||
}
|
||||
|
||||
var nameMatch = StringMatcher.FuzzySearch(query, title);
|
||||
var descriptionMatch = StringMatcher.FuzzySearch(query, topLevelOrAppItem.Subtitle);
|
||||
var extensionTitleMatch = StringMatcher.FuzzySearch(query, extensionDisplayName);
|
||||
var scores = new[]
|
||||
{
|
||||
nameMatch.Score,
|
||||
(descriptionMatch.Score - 4) / 2.0,
|
||||
isFallback ? 1 : 0, // Always give fallbacks a chance...
|
||||
};
|
||||
var max = scores.Max();
|
||||
max = max + (extensionTitleMatch.Score / 1.5);
|
||||
|
||||
// ... but downweight them
|
||||
var matchSomething = (max / (isFallback ? 3 : 1))
|
||||
+ (isAliasMatch ? 9001 : (isAliasSubstringMatch ? 1 : 0));
|
||||
|
||||
// If we matched title, subtitle, or alias (something real), then
|
||||
// here we add the recent command weight boost
|
||||
//
|
||||
// Otherwise something like `x` will still match everything you've run before
|
||||
var finalScore = matchSomething;
|
||||
if (matchSomething > 0)
|
||||
{
|
||||
var history = _serviceProvider.GetService<AppStateModel>()!.RecentCommands;
|
||||
var recentWeightBoost = history.GetCommandHistoryWeight(id);
|
||||
finalScore += recentWeightBoost;
|
||||
}
|
||||
|
||||
return (int)finalScore;
|
||||
}
|
||||
|
||||
public void UpdateHistory(IListItem topLevelOrAppItem)
|
||||
{
|
||||
var id = IdForTopLevelOrAppItem(topLevelOrAppItem);
|
||||
var state = _serviceProvider.GetService<AppStateModel>()!;
|
||||
var history = state.RecentCommands;
|
||||
history.AddHistoryItem(id);
|
||||
AppStateModel.SaveState(state);
|
||||
}
|
||||
|
||||
private string IdForTopLevelOrAppItem(IListItem topLevelOrAppItem)
|
||||
{
|
||||
if (topLevelOrAppItem is TopLevelCommandItemWrapper toplevel)
|
||||
{
|
||||
return toplevel.Id;
|
||||
}
|
||||
else
|
||||
{
|
||||
// we've got an app here
|
||||
return topLevelOrAppItem.Command?.Id ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(ClearSearchMessage message) => SearchText = string.Empty;
|
||||
|
||||
public void Receive(UpdateFallbackItemsMessage message) => RaiseItemsChanged(_tlcManager.TopLevelCommands.Count);
|
||||
|
||||
private void SettingsChangedHandler(SettingsModel sender, object? args) => HotReloadSettings(sender);
|
||||
|
||||
private void HotReloadSettings(SettingsModel settings) => ShowDetails = settings.ShowAppDetails;
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
// 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.IO.Compression;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.BuiltinCommands;
|
||||
|
||||
internal sealed partial class NewExtensionForm : NewExtensionFormBase
|
||||
{
|
||||
private static readonly string _creatingText = "Creating new extension...";
|
||||
private readonly StatusMessage _creatingMessage = new()
|
||||
{
|
||||
Message = _creatingText,
|
||||
Progress = new ProgressState() { IsIndeterminate = true },
|
||||
};
|
||||
|
||||
public NewExtensionForm()
|
||||
{
|
||||
TemplateJson = $$"""
|
||||
{
|
||||
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
"type": "AdaptiveCard",
|
||||
"version": "1.6",
|
||||
"body": [
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "{{Properties.Resources.builtin_create_extension_page_title}}",
|
||||
"size": "large"
|
||||
},
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "{{Properties.Resources.builtin_create_extension_page_text}}",
|
||||
"wrap": true
|
||||
},
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "{{Properties.Resources.builtin_create_extension_name_header}}",
|
||||
"weight": "bolder",
|
||||
"size": "default"
|
||||
},
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "{{Properties.Resources.builtin_create_extension_name_description}}",
|
||||
"wrap": true
|
||||
},
|
||||
{
|
||||
"type": "Input.Text",
|
||||
"label": "{{Properties.Resources.builtin_create_extension_name_label}}",
|
||||
"isRequired": true,
|
||||
"errorMessage": "{{Properties.Resources.builtin_create_extension_name_required}}",
|
||||
"id": "ExtensionName",
|
||||
"placeholder": "ExtensionName",
|
||||
"regex": "^[^\\s]+$"
|
||||
},
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "{{Properties.Resources.builtin_create_extension_display_name_header}}",
|
||||
"weight": "bolder",
|
||||
"size": "default"
|
||||
},
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "{{Properties.Resources.builtin_create_extension_display_name_description}}",
|
||||
"wrap": true
|
||||
},
|
||||
{
|
||||
"type": "Input.Text",
|
||||
"label": "{{Properties.Resources.builtin_create_extension_display_name_label}}",
|
||||
"isRequired": true,
|
||||
"errorMessage": "{{Properties.Resources.builtin_create_extension_display_name_required}}",
|
||||
"id": "DisplayName",
|
||||
"placeholder": "My new extension"
|
||||
},
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "{{Properties.Resources.builtin_create_extension_directory_header}}",
|
||||
"weight": "bolder",
|
||||
"size": "default"
|
||||
},
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "{{Properties.Resources.builtin_create_extension_directory_description}}",
|
||||
"wrap": true
|
||||
},
|
||||
{
|
||||
"type": "Input.Text",
|
||||
"label": "{{Properties.Resources.builtin_create_extension_directory_label}}",
|
||||
"isRequired": true,
|
||||
"errorMessage": "{{Properties.Resources.builtin_create_extension_directory_required}}",
|
||||
"id": "OutputPath",
|
||||
"placeholder": "C:\\users\\me\\dev"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": "Action.Submit",
|
||||
"title": "{{Properties.Resources.builtin_create_extension_submit}}",
|
||||
"associatedInputs": "auto"
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
}
|
||||
|
||||
public override CommandResult SubmitForm(string payload)
|
||||
{
|
||||
var formInput = JsonNode.Parse(payload)?.AsObject();
|
||||
if (formInput == null)
|
||||
{
|
||||
return CommandResult.KeepOpen();
|
||||
}
|
||||
|
||||
var extensionName = formInput["ExtensionName"]?.AsValue()?.ToString() ?? string.Empty;
|
||||
var displayName = formInput["DisplayName"]?.AsValue()?.ToString() ?? string.Empty;
|
||||
var outputPath = formInput["OutputPath"]?.AsValue()?.ToString() ?? string.Empty;
|
||||
|
||||
_creatingMessage.State = MessageState.Info;
|
||||
_creatingMessage.Message = _creatingText;
|
||||
_creatingMessage.Progress = new ProgressState() { IsIndeterminate = true };
|
||||
BuiltinsExtensionHost.Instance.ShowStatus(_creatingMessage, StatusContext.Extension);
|
||||
|
||||
try
|
||||
{
|
||||
CreateExtension(extensionName, displayName, outputPath);
|
||||
|
||||
BuiltinsExtensionHost.Instance.HideStatus(_creatingMessage);
|
||||
|
||||
RaiseFormSubmit(new CreatedExtensionForm(extensionName, displayName, outputPath));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
BuiltinsExtensionHost.Instance.HideStatus(_creatingMessage);
|
||||
|
||||
_creatingMessage.State = MessageState.Error;
|
||||
_creatingMessage.Message = $"Error: {e.Message}";
|
||||
}
|
||||
|
||||
return CommandResult.KeepOpen();
|
||||
}
|
||||
|
||||
private void CreateExtension(string extensionName, string newDisplayName, string outputPath)
|
||||
{
|
||||
var newGuid = Guid.NewGuid().ToString();
|
||||
|
||||
// Unzip `template.zip` to a temp dir:
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
|
||||
|
||||
// Does the output path exist?
|
||||
if (!Directory.Exists(outputPath))
|
||||
{
|
||||
Directory.CreateDirectory(outputPath);
|
||||
}
|
||||
|
||||
var assetsPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory.ToString(), "Microsoft.CmdPal.UI.ViewModels\\Assets\\template.zip");
|
||||
ZipFile.ExtractToDirectory(assetsPath, tempDir);
|
||||
|
||||
var files = Directory.GetFiles(tempDir, "*", SearchOption.AllDirectories);
|
||||
foreach (var file in files)
|
||||
{
|
||||
var text = File.ReadAllText(file);
|
||||
|
||||
// Replace all the instances of `FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF` with a new random guid:
|
||||
text = text.Replace("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF", newGuid);
|
||||
|
||||
// Then replace all the `TemplateCmdPalExtension` with `extensionName`
|
||||
text = text.Replace("TemplateCmdPalExtension", extensionName);
|
||||
|
||||
// Then replace all the `TemplateDisplayName` with `newDisplayName`
|
||||
text = text.Replace("TemplateDisplayName", newDisplayName);
|
||||
|
||||
// We're going to write the file to the same relative location in the output path
|
||||
var relativePath = Path.GetRelativePath(tempDir, file);
|
||||
|
||||
var newFileName = Path.Combine(outputPath, relativePath);
|
||||
|
||||
// if the file name had `TemplateCmdPalExtension` in it, replace it with `extensionName`
|
||||
newFileName = newFileName.Replace("TemplateCmdPalExtension", extensionName);
|
||||
|
||||
// Make sure the directory exists
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(newFileName)!);
|
||||
|
||||
File.WriteAllText(newFileName, text);
|
||||
|
||||
// Delete the old file
|
||||
File.Delete(file);
|
||||
}
|
||||
|
||||
// Delete the temp dir
|
||||
Directory.Delete(tempDir, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// 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.Diagnostics.CodeAnalysis;
|
||||
using System.IO.Compression;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.BuiltinCommands;
|
||||
|
||||
internal abstract partial class NewExtensionFormBase : FormContent
|
||||
{
|
||||
public event TypedEventHandler<NewExtensionFormBase, NewExtensionFormBase?>? FormSubmitted;
|
||||
|
||||
protected void RaiseFormSubmit(NewExtensionFormBase? next) => FormSubmitted?.Invoke(this, next);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// 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;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.BuiltinCommands;
|
||||
|
||||
public partial class NewExtensionPage : ContentPage
|
||||
{
|
||||
private NewExtensionForm _inputForm = new();
|
||||
private NewExtensionFormBase? _resultForm;
|
||||
|
||||
public override IContent[] GetContent()
|
||||
{
|
||||
return _resultForm != null ? [_resultForm] : [_inputForm];
|
||||
}
|
||||
|
||||
public NewExtensionPage()
|
||||
{
|
||||
Name = Properties.Resources.builtin_create_extension_name;
|
||||
Title = Properties.Resources.builtin_create_extension_title;
|
||||
Icon = new IconInfo("\uEA86"); // Puzzle
|
||||
|
||||
_inputForm.FormSubmitted += FormSubmitted;
|
||||
}
|
||||
|
||||
private void FormSubmitted(NewExtensionFormBase sender, NewExtensionFormBase? args)
|
||||
{
|
||||
if (_resultForm != null)
|
||||
{
|
||||
_resultForm.FormSubmitted -= FormSubmitted;
|
||||
}
|
||||
|
||||
_resultForm = args;
|
||||
if (_resultForm != null)
|
||||
{
|
||||
_resultForm.FormSubmitted += FormSubmitted;
|
||||
}
|
||||
else
|
||||
{
|
||||
_inputForm = new();
|
||||
_inputForm.FormSubmitted += FormSubmitted;
|
||||
}
|
||||
|
||||
RaiseItemsChanged(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// 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 CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.BuiltinCommands;
|
||||
|
||||
public partial class OpenSettingsCommand : InvokableCommand
|
||||
{
|
||||
public OpenSettingsCommand()
|
||||
{
|
||||
Name = Properties.Resources.builtin_open_settings_name;
|
||||
Icon = new IconInfo("\uE713");
|
||||
}
|
||||
|
||||
public override ICommandResult Invoke()
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<OpenSettingsMessage>();
|
||||
return CommandResult.KeepOpen();
|
||||
}
|
||||
}
|
||||
@@ -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 CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.BuiltinCommands;
|
||||
|
||||
public partial class QuitCommand : InvokableCommand, IFallbackHandler
|
||||
{
|
||||
public QuitCommand()
|
||||
{
|
||||
Icon = new IconInfo("\uE711");
|
||||
}
|
||||
|
||||
public override ICommandResult Invoke()
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<QuitMessage>();
|
||||
return CommandResult.KeepOpen();
|
||||
}
|
||||
|
||||
// this sneaky hidden behavior, I'm not event gonna try to localize this.
|
||||
public void UpdateQuery(string query) => Name = query.StartsWith('q') ? "Quit" : string.Empty;
|
||||
}
|
||||
@@ -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 CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.BuiltinCommands;
|
||||
|
||||
public partial class ReloadExtensionsCommand : InvokableCommand
|
||||
{
|
||||
public ReloadExtensionsCommand()
|
||||
{
|
||||
Icon = new IconInfo("\uE72C"); // Refresh icon
|
||||
}
|
||||
|
||||
public override ICommandResult Invoke()
|
||||
{
|
||||
// 1% BODGY: clear the search before reloading, so that we tell in-proc
|
||||
// fallback handlers the empty search text
|
||||
WeakReferenceMessenger.Default.Send<ClearSearchMessage>();
|
||||
WeakReferenceMessenger.Default.Send<ReloadCommandsMessage>();
|
||||
return CommandResult.GoHome();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user