mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-04 10:16:24 +02:00
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>
2240 lines
89 KiB
Markdown
2240 lines
89 KiB
Markdown
---
|
|
author: Mike Griese
|
|
created on: 2024-07-19
|
|
last updated: 2025-03-10
|
|
issue id: n/a
|
|
---
|
|
|
|
# Run v2 Extensions SDK
|
|
|
|
_aka "DevPal", "PT Run v2", "DevSearch", "Windows Command Palette", this thing has many names. I'll use "DevPal" throughout the doc_
|
|
|
|
> [NOTE!]
|
|
> Are you here to just see what the SDK looks like? Skip to the [Commands
|
|
> SDK details](#commands-sdk-details) section.
|
|
|
|
## Abstract
|
|
|
|
"DevPal" is "PowerToys Run v2" - the graduated form of PowerToys Run, ready for
|
|
ingestion as a Windows inbox app. DevPal enables the user to launch more than
|
|
just apps and search for files - it's a highly customizable surface that allows
|
|
users to "Start _anything_".
|
|
|
|
Most importantly, DevPal is highly extensible. By exposing a simple-to-use SDK,
|
|
DevPal enables 3p developers to quickly create plugins and extend it
|
|
functionality.
|
|
|
|
- [Run v2 Extensions SDK](#run-v2-extensions-sdk)
|
|
- [Abstract](#abstract)
|
|
- [Background](#background)
|
|
- [Inspiration](#inspiration)
|
|
- [User Stories](#user-stories)
|
|
- [Elevator Pitch](#elevator-pitch)
|
|
- [Business Justification](#business-justification)
|
|
- [Scenario Details](#scenario-details)
|
|
- [Extension basics](#extension-basics)
|
|
- [Installed extension discovery](#installed-extension-discovery)
|
|
- [Unpackaged extensions](#unpackaged-extensions)
|
|
- [Extension lifecycle](#extension-lifecycle)
|
|
- [Startup](#startup)
|
|
- [Caching](#caching)
|
|
- [Loading commands from stub items](#loading-commands-from-stub-items)
|
|
- [Microwaved commands](#microwaved-commands)
|
|
- [Disposing](#disposing)
|
|
- [Installing extensions](#installing-extensions)
|
|
- [From the Store](#from-the-store)
|
|
- [From winget](#from-winget)
|
|
- [Publishing extensions](#publishing-extensions)
|
|
- [Built-in commands](#built-in-commands)
|
|
- [SDK overview](#sdk-overview)
|
|
- [Commands SDK details](#commands-sdk-details)
|
|
- [Commands](#commands)
|
|
- [Results](#results)
|
|
- [Pages](#pages)
|
|
- [List Pages](#list-pages)
|
|
- [Updating the list](#updating-the-list)
|
|
- [Empty content](#empty-content)
|
|
- [Filtering the list](#filtering-the-list)
|
|
- [Content Pages](#content-pages)
|
|
- [Markdown Content](#markdown-content)
|
|
- [Form Content](#form-content)
|
|
- [Other types](#other-types)
|
|
- [`ContextItem`s](#contextitems)
|
|
- [Icons - `IconInfo` and `IconData`](#icons---iconinfo-and-icondata)
|
|
- [`OptionalColor`](#optionalcolor)
|
|
- [`Details`](#details)
|
|
- [`INotifyPropChanged`](#inotifypropchanged)
|
|
- [`ICommandProvider`](#icommandprovider)
|
|
- [Fallback commands](#fallback-commands)
|
|
- [`GetCommand`](#getcommand)
|
|
- [Settings](#settings)
|
|
- [Helper SDK Classes](#helper-sdk-classes)
|
|
- [Default implementations](#default-implementations)
|
|
- [Using the Clipboard](#using-the-clipboard)
|
|
- [Settings helpers](#settings-helpers)
|
|
- [Advanced scenarios](#advanced-scenarios)
|
|
- [Status messages](#status-messages)
|
|
- [Rendering of ICommandItems in Lists and Menus](#rendering-of-icommanditems-in-lists-and-menus)
|
|
- [Class diagram](#class-diagram)
|
|
- [Future considerations](#future-considerations)
|
|
- [Arbitrary parameters and arguments](#arbitrary-parameters-and-arguments)
|
|
- [URI activation](#uri-activation)
|
|
- [Custom "empty list" messages](#custom-empty-list-messages)
|
|
- [Footnotes](#footnotes)
|
|
- [Generating the `.idl`](#generating-the-idl)
|
|
- [Adding APIs](#adding-apis)
|
|
|
|
|
|
## Background
|
|
|
|
> [!NOTE]
|
|
> This is the spec specifically for the devpal SDK. For a more general overview of devpal, see the [this functional spec](https://microsoft.sharepoint-df.com/:w:/r/teams/windows/_layouts/15/Doc.aspx?sourcedoc=%7BB2D5260A-C01C-4F9E-BD58-0D69FC75FE0D%7D&file=Developer%20Search%20Functional%20Spec%20v2.docx&action=default&mobileredirect=true) (internal only).
|
|
|
|
### Inspiration
|
|
|
|
The largest inspiration for this extension SDK is the [Dev Home Extension]
|
|
model. They are the ones who pioneered the plumbing for registering COM classes
|
|
in the extension manifest, then using `CoCreateInstance` to create objects in
|
|
the host and use them as WinRT objects.
|
|
|
|
### User Stories
|
|
|
|
_(typically there'd be a long list of user stories here, but that felt better
|
|
suited for a more general DevPal dev spec, rather than the SDK doc)_
|
|
|
|
### Elevator Pitch
|
|
|
|
> "Start _anything_ here".
|
|
|
|
What if the Start Menu was more than just a launcher for apps? What if it could
|
|
be the start for all sorts of different workflows? One that apps could plug into
|
|
directly, and provide dedicated experiences for their users.
|
|
|
|
## Business Justification
|
|
|
|
It will delight ~developers~ all power users.
|
|
|
|
## Scenario Details
|
|
|
|
> [!NOTE]
|
|
>
|
|
> This document is largely concerned with the details for how 3p apps could plug
|
|
> into DevPal. However, much of the built-in devpal functionality will be built
|
|
> using the same interfaces. This will make sure that everything that we build
|
|
> keeps 3p use cases in mind. Built-in experiences, however, can be loaded
|
|
> in-proc, so they can skip pretty much all of this doc up till "[SDK
|
|
> overview](#sdk-overview)".
|
|
|
|
### Extension basics
|
|
|
|
In the simplest case, extensions for Dev Pal can register themselves using their `.appxmanifest`. As an example:
|
|
|
|
```xml
|
|
<Extensions>
|
|
<com:Extension Category="windows.comServer">
|
|
<com:ComServer>
|
|
<com:ExeServer Executable="ExtensionName.exe" Arguments="-RegisterProcessAsComServer" DisplayName="Sample Extension">
|
|
<com:Class Id="<Extension CLSID Here>" DisplayName="Sample Extension" />
|
|
</com:ExeServer>
|
|
</com:ComServer>
|
|
</com:Extension>
|
|
<uap3:Extension Category="windows.appExtension">
|
|
<uap3:AppExtension Name="com.microsoft.commandpalette"
|
|
Id="YourApplicationUniqueId"
|
|
PublicFolder="Public"
|
|
DisplayName="Sample Extension"
|
|
Description="Sample Extension for Run">
|
|
<uap3:Properties>
|
|
<CmdPalProvider>
|
|
<Activation>
|
|
<CreateInstance ClassId="<Extension CLSID Here>" />
|
|
</Activation>
|
|
<SupportedInterfaces>
|
|
<Commands />
|
|
</SupportedInterfaces>
|
|
</CmdPalProvider>
|
|
</uap3:Properties>
|
|
</uap3:AppExtension>
|
|
</uap3:Extension>
|
|
</Extensions>
|
|
```
|
|
|
|
Notable elements:
|
|
* The application must specify a `Extensions.comExtension.ComServer` to host
|
|
their COM class. This allows for the OS to register that GUID as a COM class
|
|
we can instantiate.
|
|
* Make sure that this CLSID is unique, and matches the one in your application
|
|
* The application must specify a `Extensions.uap3Extension.AppExtension` with
|
|
the Name set to `com.microsoft.commandpalette`. This is the unique identifier which
|
|
DevPal can use to find it's extensions.
|
|
* In the `Properties` of your `AppExtension`, you must specify a
|
|
`CmdPalProvider` element. This is where you specify the CLSID of the COM class
|
|
that DevPal will instantiate to interact with your extension. Also, you
|
|
specify which interfaces you support.
|
|
|
|
Currently, only `Commands` is supported. If we need to add more
|
|
in the future, we can add them to the `SupportedInterfaces` element.
|
|
|
|
This is all exactly the same as the Dev Home Extension model, with a different
|
|
`Name` in the `AppExtension` element, and different `SupportedInterfaces`.
|
|
|
|
### Installed extension discovery
|
|
|
|
Fortunately for DevPal, it is quite trivial to enumerate installed packages that
|
|
have registered themselves as a `AppExtension` extensions. This is done by
|
|
querying the `AppExtensionCatalog` for all extensions with the `Name` set to
|
|
`com.microsoft.commandpalette`.
|
|
|
|
#### Unpackaged extensions
|
|
|
|
[Sparse packages](https://nicksnettravels.builttoroam.com/sparse-package/) are
|
|
always a simple solution for adding package identity to otherwise unpackaged
|
|
applications. However, there may be apps out there that (for whatever reason)
|
|
still don't have a package identity. We need a way to allow these apps to
|
|
register themselves as extensions.
|
|
|
|
We can't just ask the COM catalog for all CLSIDs that implement a particular
|
|
COM interface, unfortunately. This means we'll need another well-known location
|
|
in the registry for unpackaged apps to write their extension CLSID's into.
|
|
|
|
We'll create a registry key at
|
|
`HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\DevPal\Extensions` [TODO!api-review]: not this path. I think that's owned by the OS
|
|
with a subkey for each extension. The subkey should be the `Name` of the
|
|
extension, and the default value should be the CLSID of the COM class that
|
|
implements the extension. On startup, DevPal will enumerate these keys in
|
|
addition to the packaged ones. Apps should not write into both locations, unless
|
|
they have two separate extension classes to load.
|
|
|
|
On uninstall, apps should remove themselves from that registry key.
|
|
|
|
### Extension lifecycle
|
|
|
|
#### Startup
|
|
|
|
When an extension is installed, DevPal will find it in the
|
|
`AppExtensionCatalog`, and parse the `AppExtension.Properties`. It will then
|
|
instantiate the COM class specified in the `CreateInstance` element. It is
|
|
ultimately up to the extension app how it wants to serve that COM class. In the
|
|
simplest case (above), COM will create the `ExtensionName.exe` process to serve
|
|
the COM object.
|
|
|
|
A more elaborate use case would have an existing process that the COM object is
|
|
created in. This is useful for extensions that need to deeply tie into a running
|
|
application. One could imagine a Windows Terminal extension that produces a list
|
|
of all the open windows, tabs and panes. This extension would be best served by
|
|
the Terminal process itself, as it has all the context it needs to produce that
|
|
list.
|
|
|
|
When DevPal launches, it will enumerate all the extensions it knows about, and
|
|
create the `IExtension` object for each one. DevPal will then get the
|
|
`ICommandProvider` for apps that register as `Commands` providers in
|
|
`SupportedInterfaces`. Extension apps should have that COM object served
|
|
quickly, for performance. That is the first object that DevPal needs, to load
|
|
the top-level list of commands.
|
|
|
|
These commands will be loaded asynchronously, and the UI will be updated as they
|
|
are loaded on a cold launch. Subsequent launches will have devpal already
|
|
running in the background.
|
|
|
|
Each individual page will be loaded as the user navigates to it. This allows for
|
|
apps to lazily load their "UI content" as needed.
|
|
|
|
#### Caching
|
|
|
|
A key goal of DevPal is to have it start as fast as possible, and have the
|
|
footprint while it's running be minimal. If DevPal needed to instantiate every
|
|
extension just to get the top-level list of commands, the impact on startup
|
|
would grow as the user installed more extensions. Additionally, each extension
|
|
installed would result in another process starting and running for the lifetime
|
|
of DevPal.
|
|
|
|
To avoid this, DevPal can "cache" the toplevel commands for an extension. This
|
|
will allow DevPal to display these commands to the user on startup, without
|
|
needing to keep the extension process running.
|
|
|
|
Command Providers which can have their commands cached are called "**frozen**",
|
|
and have `Frozen=true` set in `ICommandProvider`. This is the _default_ for apps
|
|
using the helpers library. For the large majority of extensions, the list of
|
|
commands won't change over time. These extensions can be cached to save
|
|
resources.
|
|
|
|
Command providers can opt out of this behavior by setting `Frozen=false` in
|
|
their extension. We'll call these extensions "**fresh, never frozen**".
|
|
|
|
As some examples:
|
|
* The "Hacker News" extension, only has a single top-level command. Once we load
|
|
that once, we don't need to `CreateProcess` just to find that command title.
|
|
This is a **frozen** extension.
|
|
* Similarly for something like the GitHub extension - it's got multiple
|
|
top-level commands (My issues, Issue search, Repo search, etc), but these
|
|
top-level commands never change. This is a **frozen** extension.
|
|
* The "Quick Links" extension has a dynamic list of top-level commands.
|
|
This is a **fresh** extension.[^3]
|
|
* The "Media Controls" extension only has a single top-level command, but it
|
|
needs to be running to be able to update it's title and icon. So we can't just
|
|
cache the state of it. This is a **fresh** extension.
|
|
* Similarly, any top-level `IFallbackHandler` need to be running to get
|
|
real-time updates to their name. Any extension with a top-level
|
|
`IFallbackHandler` will be treated as **fresh** extension.
|
|
|
|
How do we actually cache this frozen list?
|
|
|
|
Our goal is to make initial startup to be fast, even faster than hitting the
|
|
package catalog. To support this, DevPal will cache information about the
|
|
top-level commands it gets from extensions. With that cached information, DevPal
|
|
can create "stub" entries of those commands in the palette, without launching
|
|
the extension first.
|
|
|
|
The structure of the data DevPal caches will look something like the following:
|
|
|
|
```jsonc
|
|
{
|
|
"extensions": [
|
|
{
|
|
"pfn": "TemplateExtension_0.0.1.0_x64__8wekyb3d8bbwe",
|
|
"clsid": "{FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF}",
|
|
|
|
"displayName": "TemplateDisplayName",
|
|
"frozen": true,
|
|
"commands":
|
|
[
|
|
{
|
|
"id": "TemplateExtension.MyFirstCommand",
|
|
"icon": "",
|
|
"title": "",
|
|
"subtitle": "",
|
|
"name": "",
|
|
"moreCommands": [ /* Additional commands... */]
|
|
}
|
|
]
|
|
}
|
|
]
|
|
|
|
}
|
|
```
|
|
|
|
In this data you can see:
|
|
* We cache some basic info about each extension we've seen. This includes
|
|
* the Package Family Name (a unique identifier per-app package),
|
|
* the COM CLSID for that extension,
|
|
* the display name for that extension,
|
|
* and if that extension is frozen or not.
|
|
* We also cache the list of top-level commands for that extension. We'll store
|
|
the basic amount of info we need to recreate that command in the top-level
|
|
list.
|
|
|
|
On a cold launch, DevPal will do the following:
|
|
|
|
1. SLOW: First we start up WASDK and XAML. Unavoidable cost.
|
|
2. FAST: We load builtin extensions. These are just extensions in DLLs, so
|
|
there's nothing to it.
|
|
3. FAST: We load our cache of extensions from disk, and note which are frozen vs fresh
|
|
* We needs this to know which extensions have classes we need to instantiate
|
|
(fresh), vs ones we can lazy-start (frozen)
|
|
* We'll create stub extension objects here, with the CLSID ~and commandline~ stored.
|
|
* note: we don't need the executable/commandline. Just the clsid. We still
|
|
want to leave it to COM to deal with actually starting the COM server.
|
|
That's not our job, and we don't need the package catalog loaded to get
|
|
that - only the clsid.
|
|
4. FAST: With our extension & command cache, get the list of commands, and add
|
|
those items as "stub" list items
|
|
* These items don't have an actual _live_ extension object backing them.
|
|
They're just the data we cached from the extension.
|
|
* Stubs refer to the stub extension object we created in 3
|
|
* When the user clicks on a stub item, we'll do what's outlined in [Loading commands from stub items](#loading-commands-from-stub-items) below.
|
|
5. SLOW: We open the package catalog for more commands
|
|
* Extensions that we've seen before in our cache:
|
|
* If it's fresh, we'll start it, and fill in commands from `TopLevelCommands` into the palette
|
|
* If it's frozen, we'll leave it be. We've already got stubs for it.
|
|
* Extensions we've never seen before:
|
|
* Start it up.
|
|
* Check if it's fresh or frozen.
|
|
* Call `TopLevelCommands`, and put all of them in the list
|
|
* Create a extension cache entry for that app.
|
|
* If the provider is frozen: we can actually release the
|
|
`ICommandProvider` instance at this point.
|
|
* And of course, if we don't find all the packages we had cached, then delete
|
|
entries for the missing ones. Those apps were uninstalled.
|
|
6. We start a package catalog change watcher to be notified by the OS for
|
|
changes to the list of installed extensions
|
|
|
|
After 1, we can display the UI. It won't have any commands though, so maybe we should wait.
|
|
After 2, we'd have some commands, but nothing from extensions
|
|
After 4, the palette is ready to be used, with all the frozen extension commands. This is probably good enough for most use cases.
|
|
|
|
Most of the time, when the user "launches" devPal, we won't run through this
|
|
whole process. The slowest part of startup is standing up WASDK and WinUI. After
|
|
the first launch, we will keep our window will be hidden, running silently in
|
|
the background. When the user presses the global hotkey, or otherwise launches
|
|
the app, we'll just activate our existing process. This is a "warm launch".
|
|
|
|
On a warm launch, we already have all the stubs in memory. We're listening for
|
|
package installs/uninstalls already. So we're basically just in the post-step 5
|
|
state already.
|
|
|
|
##### Loading commands from stub items
|
|
|
|
<sup>_"reheating"_</sup>
|
|
|
|
When the user activates a top-level "stub" list item (referring to a frozen
|
|
command), we need to quickly load that app and get the command for it.
|
|
|
|
1. If this stub has already been reheated, just return out the `ICommand` we
|
|
instantiated last time, and skip this whole process.
|
|
2. Get the extension stub out of that item
|
|
* If that fails: display an error message.
|
|
3. Check if the extension is already in the warm extension cache. If it is, we
|
|
recently reheated a command from this provider. We can skip step 4 and go
|
|
straight to step 5
|
|
4. Use the CLSID from the cache to `CoCreateInstance` this extension, and get its `ICommandProvider`.
|
|
* If that fails: display an error message.
|
|
5. Try to load the command from the provider. This is done in two steps:
|
|
1. If the cached command had an `id`, try to look up the command with
|
|
`ICommandProvider.GetCommand(id)`, passing the `id`. If that returns an
|
|
item, we can move on to the next stem
|
|
2. Otherwise (the command wasn't assigned an ID, or `GetCommand` returned
|
|
null): all `TopLevelItems` on that `CommandProvider`.
|
|
* Search through all the returned commands with the same `id` or
|
|
`icon/title/subtitle/name`, and return that one.
|
|
6. If we found the command from the provider, navigate to it or invoke it.
|
|
|
|
##### Microwaved commands
|
|
|
|
DevPal will want to keep the last N extensions that were activated "warm", so
|
|
that they are ready to be re-activated again. Once a command provider is
|
|
reheated, we'll keep it around in a warm extension cache. Stubs that have been
|
|
activated by reheating them can then skip a `CommandProvider` lookup
|
|
(`GetCommand` or `TopLevelItems`).
|
|
|
|
We'll give the user options to control how many of the most recent commands we
|
|
keep warm at a given time. We'll probably also want to offer an option like
|
|
"always warm up {this command} / {all commands}", if the user doesn't care about
|
|
the memory usage as much.
|
|
|
|
> [WARNING!]
|
|
>
|
|
> If your command provider returns a `IFallbackCommandItem`s from
|
|
> `FallbackCommands`, and that provider is marked `frozen`, DevPal will always
|
|
> treat your provider as "fresh". Otherwise, devpal wouldn't be able to call
|
|
> into the extension to have the `IFallbackHandler` respond to the search query.
|
|
>
|
|
> The alternative would be to have DevPal just ignore the fallback handler from
|
|
> that provider. Silently doing nothing seemed less user friendly than silently
|
|
> doing What's Expected.
|
|
|
|
#### Disposing
|
|
|
|
As implied by the above section, all the extensions for DevPal won't be loaded
|
|
and running all at once. We'll only keep the "fresh never frozen" extensions
|
|
running, as well as the last N most recently used commands. Once we've exceeded
|
|
that limit of recent commands, we'll release our reference to the COM object for
|
|
that extension, and re-mark commands from it as "stubs". Upon the release of
|
|
that reference, the extension is free to clean itself up. For extensions that
|
|
use the helpers library, they can override `CommandProvider.Dispose` to do
|
|
cleanup in there.
|
|
|
|
## Installing extensions
|
|
|
|
### From the Store
|
|
|
|
These are fairly straightforward. The user goes to the Store, finds an extension
|
|
app, and installs it. When they do that, the PackageCatalog will send us an
|
|
event which we can use to update our list of extensions.
|
|
|
|
At the time of writing, there's not an easy way to query the store for a list of
|
|
apps who's manifest specifies that they are an extension. We could launch
|
|
something like:
|
|
|
|
```
|
|
ms-windows-store://assoc/?Tags=AppExtension-com.microsoft.commandpalette
|
|
```
|
|
|
|
to open the store to a list of extensions. However, we can't list those
|
|
ourselves directly. Our friends in DevHome suggested it could be possible to
|
|
stand up a azure service which could query the store for us, and return a list
|
|
of extensions. This is not something that they currently have planned, nor would
|
|
it be cheap from an engineering standpoint.
|
|
|
|
### From winget
|
|
|
|
Winget on the other hand, does allow packages to specify arbitrary tags, and let
|
|
apps query them easily. We can use that as a system to load a list of packages
|
|
available via winget directly in DevPal. We'll specify a well-known tag that
|
|
developers can use in their winget package manifest to specify that their
|
|
package is an extension. We will then query winget for all packages with that
|
|
tag, to expose a list of possible extensions.
|
|
|
|
## Publishing extensions
|
|
|
|
As a part of DevPal, we'll ship a "Sample Project" template. We will use this to
|
|
be able to quickly generate a project that can be used to create a new
|
|
extension. This will include the `sln`, `csproj`, and `appxmanifest` files that
|
|
are needed to create a new extension, as well as plumbing to get it all ready.
|
|
All the developer will need to do is open up the project to the
|
|
`MyCommandProvider` class and start implementing their commands.
|
|
|
|
As a part of this project template, we'll include a `winget` GitHub Actions
|
|
workflow that will allow the developer to push their extension to winget with
|
|
the needed tags to be discoverable by DevPal. That way, developers won't need to
|
|
worry about the details of packaging, they can just push to main and let the
|
|
pipeline take care of the rest.
|
|
|
|
## Built-in commands
|
|
|
|
Certain commands are "built-in" to DevPal. These are commands that ship directly
|
|
in the DevPal package. These don't need to be loaded as out-of-proc COM
|
|
extensions like the third-party ones. However, they will still conform to the
|
|
same interface as third-party commands. This allows us to have a consistent
|
|
experience for the user. It also cements the ability for 3p extensions to do
|
|
anything that a 1p built-in can do.
|
|
|
|
## SDK overview
|
|
|
|
The SDK for DevPal is split into two namespaces:
|
|
* `Microsoft.CommandPalette.Extensions` - This namespace contains the interfaces that
|
|
developers will implement to create extensions for DevPal.
|
|
* `Microsoft.CommandPalette.Extensions.Toolkit` - This namespace contains helper classes
|
|
that developers can use to make creating extensions easier.
|
|
|
|
The first is highly abstract, and gives developers total control over the
|
|
implementation of their extension. The second contains a variety of default
|
|
implementations and helper classes that developers can use to make authoring
|
|
extensions simpler.
|
|
|
|
## Commands SDK details
|
|
|
|
Below details the SDK that developers can use to create extensions for the
|
|
DevPal. These interfaces are exposed through the `Microsoft.CommandPalette.Extensions`
|
|
namespace. We'll expose an SDK with helper classes and default implementations
|
|
in the `Microsoft.CommandPalette.Extensions` namespace.
|
|
|
|
> [NOTE!]
|
|
>
|
|
> In the following SDK details, `csharp` & `c#` code fences to show snippets of
|
|
> what the `Microsoft.CommandPalette.Extensions` interface will look like. This is roughly
|
|
> `midl` v3 in this spec, with one modification. I'm using the made up `async`
|
|
> keyword to indicate that a method is async. In the real `.idl`, these methods
|
|
> will be replaced with `IAsyncAction` for `async void` and `IAsyncOperation<T>`
|
|
> for `async T`.
|
|
>
|
|
> `cs` code fences will be used for samples of what an extension implementations
|
|
> may look like.
|
|
|
|
### Commands
|
|
|
|
Commands are the primary unit of functionality in the DevPal SDK. They represent
|
|
"a thing that a user can do". These can be something simple like open a URL in a
|
|
web browser. Or they can be more complex, with nested commands, custom arguments,
|
|
and more.
|
|
|
|
<!-- Note to editors:
|
|
Anything in `csharp` or `c#` code fences will be pulled into the .idl.
|
|
Use `cs` for samples.
|
|
"c#" blocks will get placed before "csharp" ones. -->
|
|
|
|
```c#
|
|
interface ICommand requires INotifyPropChanged{
|
|
String Name{ get; };
|
|
String Id{ get; };
|
|
IIconInfo Icon{ get; };
|
|
}
|
|
|
|
enum CommandResultKind {
|
|
Dismiss, // Reset the palette to the main page and dismiss
|
|
GoHome, // Go back to the main page, but keep it open
|
|
GoBack, // Go back one level
|
|
Hide, // Keep this page open, but hide the palette.
|
|
KeepOpen, // Do nothing.
|
|
GoToPage, // Go to another page. GoToPageArgs will tell you where.
|
|
ShowToast, // Display a transient message to the user
|
|
Confirm, // Display a confirmation dialog
|
|
};
|
|
|
|
enum NavigationMode {
|
|
Push, // Push the target page onto the navigation stack
|
|
GoBack, // Go back one page before navigating to the target page
|
|
GoHome, // Go back to the home page before navigating to the target page
|
|
};
|
|
|
|
[uuid("f9d6423b-bd5e-44bb-a204-2f5c77a72396")]
|
|
interface ICommandResultArgs{};
|
|
interface ICommandResult {
|
|
CommandResultKind Kind { get; };
|
|
ICommandResultArgs Args { get; };
|
|
}
|
|
interface IGoToPageArgs requires ICommandResultArgs{
|
|
String PageId { get; };
|
|
NavigationMode NavigationMode { get; };
|
|
}
|
|
interface IToastArgs requires ICommandResultArgs{
|
|
String Message { get; };
|
|
ICommandResult Result { get; };
|
|
}
|
|
interface IConfirmationArgs requires ICommandResultArgs{
|
|
String Title { get; };
|
|
String Description { get; };
|
|
ICommand PrimaryCommand { get; };
|
|
Boolean IsPrimaryCommandCritical { get; };
|
|
}
|
|
|
|
// This is a "leaf" of the UI. This is something that can be "done" by the user.
|
|
// * A ListPage
|
|
// * the MoreCommands flyout of for a ListItem or a MarkdownPage
|
|
interface IInvokableCommand requires ICommand {
|
|
ICommandResult Invoke(Object sender);
|
|
}
|
|
|
|
```
|
|
|
|
If a developer wants to add a simple command to DevPal, they can create a
|
|
class that implements `ICommand`, and implement the `Invoke` method. This
|
|
method will be called when the user selects the command in DevPal.
|
|
|
|
As a simple example[^1]:
|
|
|
|
```cs
|
|
class HackerNewsPage : Microsoft.CommandPalette.Extensions.Toolkit.InvokableCommand {
|
|
public class HackerNewsPage()
|
|
{
|
|
Name = "Hacker News";
|
|
Icon = "https://news.ycombinator.com/favicon.ico";
|
|
}
|
|
|
|
public ICommandResult Invoke() {
|
|
Process.Start(new ProcessStartInfo("https://news.ycombinator.com/") { UseShellExecute = true });
|
|
return CommandResult.Hide();
|
|
}
|
|
}
|
|
```
|
|
|
|
This will create a single command in DevPal that, when selected, will open
|
|
Hacker News in the user's default web browser.
|
|
|
|
Commands can also be `Page`s, which represent additional "nested" pages within
|
|
DevPal. When the user selects an command that implements `IPage`, DevPal will
|
|
navigate to a page for that command, rather than calling `Invoke` on it. Skip
|
|
ahead to [Pages](#Pages) for more information on the different types of pages.
|
|
|
|
The `Id` property is optional. This can be set but the extension author to
|
|
support more efficient command lookup in
|
|
[`ICommandProvider.GetCommand()`, below](#getcommand).
|
|
|
|
When `Invoke` is called, the host app will pass in a `sender` object that
|
|
represents the context of where that command was invoked from. This can be
|
|
different types depending on where the command is being used:
|
|
|
|
* `TopLevelCommands` (and fallbacks)
|
|
* Sender is the `ICommandItem` for the top-level command that was invoked
|
|
* `IListPage.GetItems`
|
|
* Sender is the `IListItem` for the list item selected for that command
|
|
* `ICommandItem.MoreCommands` (context menus)
|
|
* Sender is the `IListItem` which the command was attached to for a list page, or
|
|
* the `ICommandItem` of the top-level command (if this is a context item on a top level command)
|
|
* `IContentPage.Commands`
|
|
* Sender is the `IContentPage` itself
|
|
|
|
The helpers library also exposes a `Invoke()` method on `InvokableCommand` which
|
|
takes no parameters, as a convenience for developers who don't need the `sender`
|
|
object.
|
|
|
|
Using the `sender` parameter can be useful for big lists of items where the
|
|
actionable information for each item is practically the same. Consider a big
|
|
list of links. An extension developer can implement this as a single
|
|
`IInvokableCommand` that opens a URL based on the `sender` object passed in.
|
|
Then each list item would store the URL to open and the title of the link. This
|
|
creates less overhead for the extension and host to communicate.
|
|
|
|
#### Results
|
|
|
|
Commands can return a `CommandResult` to indicate what DevPal should do after
|
|
the command is executed. This allows for commands to control the flow of the
|
|
DevPal. For example, an action that opens a URL might return `Kind =
|
|
CommandResult.Dismiss` to close DevPal after the URL is opened.
|
|
|
|
Use cases for each `CommandResultKind`:
|
|
|
|
* `Dismiss` - Close DevPal after the action is executed. All current state
|
|
is dismissed as well. On the next launch, DevPal will start from the main
|
|
page with a blank query.
|
|
* Ex: An action that opens an application. The Puser doesn't need DevPal
|
|
open after the application is opened, nor do they need the query they used
|
|
to find the action.
|
|
* `GoHome` - Navigate back to the main page of DevPal, but keep it open.
|
|
This clears out the current stack of pages, but keeps DevPal open.
|
|
* Note: if the action navigates to another application, DevPal's default
|
|
behavior is to hide itself when it loses focus. That will behave the same as
|
|
`Dismiss` in that case.
|
|
* Ex: The "Add Quick Link" command is a form. After submitting the form, the
|
|
user should be taken back to the main page, with the query cleared, leaving
|
|
the window open.
|
|
* `GoBack` - Navigate to the previous page, and keep it open. Useful for
|
|
submitting a form, then going back to the parent page of the form
|
|
* `KeepOpen` - Do nothing. This leaves the palette in its current state, with the
|
|
current page stack and query.
|
|
* Note: if the action navigates to another application, DevPal's default
|
|
behavior is to hide itself when it loses focus. When the user next activates
|
|
DevPal, it will be in the same state as when it was hidden.
|
|
* Ex: An action that opens a URL in a new tab, like the Hacker News sample.
|
|
The user might want to open multiple URLs in a row, so DevPal should
|
|
stay in its current state.
|
|
* `GoToPage` - Navigate to a different page in DevPal. The `GoToPageArgs`
|
|
will specify which page to navigate to.
|
|
* `Push`: The new page gets added to the current navigation stack. Going back
|
|
from the requested page will take you to the current page.
|
|
* `GoBack`: Go back one level, then navigate to the page. Going back from the
|
|
requested page will take you to the page before the current page.
|
|
* `GoHome`: Clear the back stack, then navigate to the page. Going back from
|
|
the requested page will take you to the home page (the L0).
|
|
* `ShowToast` - Display a transient desktop-level message to the user. This is
|
|
especially useful for displaying confirmation that an action took place, when
|
|
the palette will be closed. Consider the `CopyTextCommand` in the helpers -
|
|
this command will show a toast with the text "Copied to clipboard", then
|
|
dismiss the palette.
|
|
* Once the message is displayed, the palette will then react to the `Result`.
|
|
In the helpers library, the `ToastArgs`'s default `Result` value is
|
|
`Dismiss`.
|
|
* Only one toast can be displayed at a time. If a new toast is requested
|
|
before the previous one is dismissed, the new toast will replace the old
|
|
one. This includes if the `Result` of one `IToastArgs` is another
|
|
`IToastArgs`.
|
|
* `Confirm`: Display a confirmation dialog to the user. This is useful for
|
|
actions that are destructive or irreversible. The `ConfirmationArgs` will
|
|
specify the title, and description for the dialog. The primary button of the
|
|
dialog will activate the `Command`. If `IsPrimaryCommandCritical` is `true`,
|
|
the primary button will be red, indicating that it is a destructive action.
|
|
|
|
### Pages
|
|
|
|
Pages represent individual views in the application. They are the primary unit
|
|
of navigation and interaction. Developers can author `Command`s as a page to
|
|
provide an additional page of functionality within the application. They are not
|
|
wholly responsible for their own rendering. Rather, they provide bits of
|
|
information that the host application will then use to render the page.
|
|
|
|
```csharp
|
|
interface IPage requires ICommand {
|
|
String Title { get; };
|
|
Boolean IsLoading { get; };
|
|
|
|
OptionalColor AccentColor { get; };
|
|
}
|
|
```
|
|
|
|
When a user selects an action that implements `IPage`, DevPal will navigate
|
|
to that page, pushing it onto the UI stack.
|
|
|
|
Pages can be one of several types, each detailed below:
|
|
* [List](#List-Pages)
|
|
* [Content](#Content-Pages)
|
|
|
|
If a page returns a null or empty `Title`, DevPal will display the `Name` of the
|
|
`ICommand` instead.
|
|
|
|
Pages have a `Loading` property which they can use to indicate to DevPal that
|
|
the content is still loading. When `Loading` is `true`, DevPal will show an
|
|
indeterminate loading bar to the user. When `Loading` is `false`, DevPal will
|
|
hide the progress bar. This allows extensions which are displaying asynchronous
|
|
content to indicate that something is happening in the background.
|
|
|
|
Pages are `ICommands`, which means they also are observable via the
|
|
`INotifyPropChanged` interface. This allows the page to set `Loading` as needed
|
|
and change the value once the results are loaded.
|
|
|
|
Pages may also specify an `AccentColor`. DevPal will use this color to stylize
|
|
elements of the page with that color.
|
|
|
|
#### List Pages
|
|
|
|
Lists are the most common type of page. They represent a collection of items
|
|
which the user can quickly filter and search through.
|
|
|
|

|
|
|
|
Lists can be either "static" or "dynamic":
|
|
* A **static** list leaves devpal in charge of filtering the list of items,
|
|
based on the query the user typed.
|
|
* These are implementations of the default `IListPage`.
|
|
* In this case, DevPal will use a fuzzy string match over the `Name` of the
|
|
action, the `Subtitle`, and any `Text` on the `Tag`s.
|
|
* A **dynamic** list leaves the extension in charge of filtering the list of
|
|
items.
|
|
* These are implementations of the `IDynamicListPage` interface.
|
|
* In this case, the host app will call the setter for `SearchText` on the
|
|
`IDynamicListPage` when the user changes the query. The extension may then
|
|
raise an `ItemsChanged` event afterwards, to let the host know it needs to
|
|
fetch items again. Additionally, the host app won't do any filtering of the
|
|
results - it's the extension's responsibility to filter them.
|
|
* Ex: The GitHub extension may want to allow the user to type `is:issue
|
|
is:open`, then return a list of open issues, without string matching on
|
|
the text.
|
|
|
|
|
|
```csharp
|
|
[uuid("c78b9851-e76b-43ee-8f76-da5ba14e69a4")]
|
|
interface IContextItem {}
|
|
|
|
interface ICommandItem requires INotifyPropChanged {
|
|
ICommand Command{ get; };
|
|
IContextItem[] MoreCommands{ get; };
|
|
IIconInfo Icon{ get; };
|
|
String Title{ get; };
|
|
String Subtitle{ get; };
|
|
}
|
|
|
|
interface ICommandContextItem requires ICommandItem, IContextItem {
|
|
Boolean IsCritical { get; }; // READ: "make this red"
|
|
KeyChord RequestedShortcut { get; };
|
|
}
|
|
|
|
[uuid("924a87fc-32fe-4471-9156-84b3b30275a6")]
|
|
interface ISeparatorContextItem requires IContextItem {}
|
|
|
|
interface IListItem requires ICommandItem {
|
|
ITag[] Tags{ get; };
|
|
IDetails Details{ get; };
|
|
String Section { get; };
|
|
String TextToSuggest { get; };
|
|
}
|
|
|
|
interface IGridProperties {
|
|
Windows.Foundation.Size TileSize { get; };
|
|
}
|
|
|
|
interface IListPage requires IPage, INotifyItemsChanged {
|
|
// DevPal will be responsible for filtering the list of items, unless the
|
|
// class implements IDynamicListPage
|
|
String SearchText { get; };
|
|
String PlaceholderText { get; };
|
|
Boolean ShowDetails{ get; };
|
|
IFilters Filters { get; };
|
|
IGridProperties GridProperties { get; };
|
|
Boolean HasMoreItems { get; };
|
|
ICommandItem EmptyContent { get; };
|
|
|
|
IListItem[] GetItems();
|
|
void LoadMore();
|
|
}
|
|
|
|
interface IDynamicListPage requires IListPage {
|
|
String SearchText { set; };
|
|
}
|
|
```
|
|
|
|

|
|
|
|
Lists are comprised of a collection of `IListItems`.
|
|
|
|
|
|

|
|
|
|
> NOTE: The above diagram is from before Nov 2024. It doesn't properly include the relationship between `ICommandItems` and list items.
|
|
> A more up-to-date explainer of the elements of the UI can be found in
|
|
> ["Rendering of ICommandItems in Lists and Menus"](#rendering-of-icommanditems-in-lists-and-menus)
|
|
|
|
Each ListItem has one default `Command`. This is the command that will be run
|
|
when the user selects the item. If the IListItem has a non-null `Icon`, that
|
|
icon will be displayed in the list. If the `Icon` is null, DevPal will display
|
|
the `Icon` of the list item's `Command` instead.
|
|
|
|
ListItems may also have a list of `MoreCommands`. These are additional commands
|
|
that the user can take on the item. These will be displayed to the user in the
|
|
"More commands" flyout when the user has that item selected. As the user moves
|
|
focus through the list to select different items, we will update the UI to show
|
|
the commands for the currently selected item.
|
|
|
|

|
|
|
|
The elements of a ListPage (`IListItem`s) and the context menu
|
|
(`ICommandContextItem`) both share the same base type. Basically, they're both a
|
|
list of things which have:
|
|
* A `ICommand` to invoke or navigate to.
|
|
* a `Title` which might replace their `Command`'s `Name`,
|
|
* an `Icon` which might replace their `Command`'s `Icon`,
|
|
* A `Subtitle`, which is visible on the list, and a _tooltip_ for a context menu
|
|
* They might also have `MoreCommands`:
|
|
* For a `IListItem`, this is the context menu.
|
|
* For a ContextItem in the context menu, this creates a sub-context menu.
|
|
|
|
For more details on the structure of the `MoreCommands` property, see the
|
|
[`ContextItem`s](#contextitems) section below.
|
|
|
|
As an example, here's how the Media Controls extension adds play/pause, next &
|
|
previous track context commands to the list of items:
|
|
|
|
```cs
|
|
internal sealed class MediaListItem : ListItem
|
|
{
|
|
// Theis is an example of a ListItem that displays the currently track
|
|
// This TogglePlayMediaCommand is the default action when the user selects the item.
|
|
public MediaListItem() : base(new TogglePlayMediaCommand())
|
|
{
|
|
// These two commands make up the "More commands" flyout for the item.
|
|
this.MoreCommands = [
|
|
new CommandContextItem(new PrevNextTrackCommand(true)),
|
|
new CommandContextItem(new PrevNextTrackCommand(false))
|
|
];
|
|
|
|
GlobalSystemMediaTransportControlsSessionManager.RequestAsync().AsTask().ContinueWith(async (task) => {
|
|
var manager = task.Result;
|
|
var mediaSession = manager.GetCurrentSession();
|
|
var properties = await this.mediaSession.TryGetMediaPropertiesAsync().AsTask();
|
|
this.Title = properties.Title;
|
|
// update other things too
|
|
});
|
|
|
|
}
|
|
}
|
|
internal sealed class TogglePlayMediaCommand : InvokableCommand
|
|
{
|
|
public TogglePlayMediaCommand()
|
|
{
|
|
Name = "Play";
|
|
Icon = new("\ue768"); //play
|
|
}
|
|
|
|
public ICommandResult Invoke()
|
|
{
|
|
_ = mediaSession.TryTogglePlayPauseAsync();
|
|
return new ICommandResult(CommandResultKind.KeepOpen);
|
|
}
|
|
}
|
|
// And a similar InvokableCommand for the PrevNextTrackCommand
|
|
```
|
|
|
|
List items may also have an optional `Section` provided as a string. When
|
|
displaying items to the user, the Command Palette will group items with the same
|
|
`Section` string together in the order that the `Section`s are first seen in
|
|
the results. Many extensions will not use sections at all. If developers want to
|
|
have lots of grouped results, they're free to have as many sections as they
|
|
like.
|
|
* For example: An "Agenda" extension may want to have one section for each day,
|
|
with each section's items containing the events for the day.
|
|
* Or a Pokedex extension may want to group results by region.
|
|
|
|
Lists may either be a list of items like a traditional ListView, or they can be
|
|
a grid of items. Each of these items can be grouped into sections, which will be
|
|
displayed to the user in the order they are returned by the extension. Many
|
|
extensions will only have a single section, but if developers want to have lots
|
|
of grouped results, they're free to have as many sections as they like.
|
|
|
|
When the `GridProperties` property is set to null, DevPal will display the items
|
|
as a simple list, grouping them by section. When the `GridProperties` property
|
|
is set to a non-null value, DevPal will display the items as a grid, with each
|
|
item in the grid being a `TileSize` square. Grids are useful for showing items
|
|
that are more visual in nature, like images or icons.
|
|
|
|
Each item in the list may also include an optional `Details` property. This
|
|
allows the extension to provide additional information about the item, like a
|
|
description, a preview of a file, or a link to more information. For more
|
|
information on the structure of the `Details` property, see the
|
|
[`Details`](#details) section below.
|
|
|
|
If a list page returns a value from `PlaceholderText`, that text will be shown
|
|
as the placeholder for the filter on the page.
|
|
|
|
If a list page returns a value from `SearchText`, that text will be used to
|
|
initialize the search box on the page.
|
|
|
|
If the page returns `ShowDetails = true`, the DevPal automatically expand out
|
|
the [Details](#details) for list items with `Details` set to a non-null value.
|
|
If `ShowDetails = false`, the DevPal will not expand out the details for list
|
|
items by default, but will add a "Show details" action to the item's list of
|
|
actions (if it sets `Details`).
|
|
* For example: in the Windows Search box, when you search for an app, you get a
|
|
"details" that is pre-expanded.
|
|
* Similarly for file searches - you get a preview of the file, and metadata on
|
|
those details for the file path, last modified time, etc.
|
|
* But something like the "GitHub" extension may not want to always fetch issue
|
|
bodies to show their details by default. So it would set `ShowDetails =
|
|
false`. If the user activates the automatic "Show details" action, then the
|
|
github action can then fetch the body and show it.
|
|
|
|
Each item in the list may also provide `TextToSuggest`. This serves a similar
|
|
purpose to `result.QueryTextDisplay` in the original PowerToys Run API. When
|
|
provided, DevPal will use that text to provide a suggested search query to the
|
|
user. The user can accept that suggestion with a single keypress, and DevPal
|
|
will then set the `SearchText` on the page to that `TextToSuggest`. This is
|
|
especially useful for dynamic lists, to easily populate the entire `SearchText`
|
|
as the user navigates the list.
|
|
|
|
Consider the Windows Registry command. When the page is initially loaded, it
|
|
displays only the top-level registry keys (`HKEY_CURRENT_USER`,
|
|
`HKEY_LOCAL_MACHINE`, etc). If the user types `HKC`, the command will filter the
|
|
results down to just `HKEY_CURRENT_USER`, `HKEY_CLASSES_ROOT` and
|
|
`HKEY_CURRENT_CONFIG`. However, if the user at this point taps the right-arrow
|
|
key, DevPall will use the `TextToSuggest` from the `HKEY_CURRENT_USER`
|
|
`ListItem` to fill the `SearchText` with `"HKEY_CURRENT_USER\"`. The extension
|
|
will then be notified of this change to the search text, and can now return
|
|
results nested under `HKEY_CURRENT_USER\`.
|
|
|
|
An example list page for the Hacker News extension:
|
|
|
|
```cs
|
|
class NewsPost {
|
|
string Title;
|
|
string Url;
|
|
string CommentsUrl;
|
|
string Poster;
|
|
int Points;
|
|
}
|
|
class LinkCommand(NewsPost post) : Microsoft.CommandPalette.Extensions.Toolkit.InvokableCommand {
|
|
public string Name => "Open link";
|
|
public CommandResult Invoke() {
|
|
Process.Start(new ProcessStartInfo(post.Url) { UseShellExecute = true });
|
|
return CommandResult.KeepOpen;
|
|
}
|
|
}
|
|
class CommentCommand(NewsPost post) : Microsoft.CommandPalette.Extensions.Toolkit.InvokableCommand {
|
|
public string Name => "Open comments";
|
|
public CommandResult Invoke() {
|
|
Process.Start(new ProcessStartInfo(post.CommentsUrl) { UseShellExecute = true });
|
|
return CommandResult.KeepOpen;
|
|
}
|
|
}
|
|
class NewsListItem(NewsPost post) : Microsoft.CommandPalette.Extensions.Toolkit.ListItem {
|
|
public string Title => post.Title;
|
|
public string Subtitle => post.Poster;
|
|
public IContextItem[] Commands => [
|
|
new CommandContextItem(new LinkCommand(post)),
|
|
new CommandContextItem(new CommentCommand(post))
|
|
];
|
|
public ITag[] Tags => [ new Tag(){ Text=post.Points } ];
|
|
}
|
|
class HackerNewsPage: Microsoft.CommandPalette.Extensions.Toolkit.ListPage {
|
|
public bool Loading => true;
|
|
IListItem[] GetItems() {
|
|
List<NewsItem> items = /* do some RSS feed stuff */;
|
|
this.IsLoading = false;
|
|
return items
|
|
.Select((post) => new NewsListItem(post))
|
|
.ToArray();
|
|
}
|
|
}
|
|
```
|
|
|
|
##### Updating the list
|
|
|
|
Extension developers are able to update the list of items in real-time, by
|
|
raising an PropChanged event for the `Items` property of the `IListPage`. This
|
|
will cause DevPal to re-request the list of items via `GetItems`.
|
|
|
|
Consider for example a process list which updates in real-time. As the extension
|
|
determines that the list should change, it can raise the `Items` property
|
|
changed event.
|
|
|
|
> [!IMPORTANT]
|
|
> For extension developers: Best practice would be to cache your
|
|
> `IListItems` between calls. Minimizing the time it takes to respond to
|
|
> `GetItems` will make the UI feel more responsive.
|
|
|
|
> [!WARNING]
|
|
> We chose this API surface for a few reasons:
|
|
> * `IObservableCollection`, which has quite a good mechanism for specifying
|
|
> exactly which parts of the collection changed, isn't exposed through WinRT
|
|
> * `IObservableVector` doesn't work well across process boundaries
|
|
> * In general, all the collection WinRT objects are Considered Harmful in
|
|
> cross-proc scenarios
|
|
>
|
|
> But we want both static and dynamic lists to be able to update the results in
|
|
> real-time. Example: A process list that updates in real-time. We want to be
|
|
> able to add and remove items from the list as they start and stop.
|
|
>
|
|
> Additionally, `HasMoreItems` / `LoadMoreItems` is almost exactly
|
|
> [`ISupportIncrementalLoading`], but without 1: the async aspect, 2: forcing us
|
|
> to pull in XAML as a dependency
|
|
|
|
Here's a breakdown of how a dynamic list responds to the CmdPal. In this
|
|
example, we'll use a hypothetical GitHub issue search extension, which allows
|
|
the user to type a query and get a list of issues back.
|
|
|
|
1. CmdPal loads the `ListPage` from the extension.
|
|
2. It is a `IDynamicListPage`, so the command palette knows not to do any
|
|
host-side filtering.
|
|
3. CmdPal reads the `SearchText` from the ListPage
|
|
- it returns `is:issue is:open` as initial text
|
|
4. CmdPal reads the `HasMoreItems` from the ListPage
|
|
- it returns `true`
|
|
5. CmdPal calls `GetItems()`
|
|
- the extension returns the first 25 items that match the query.
|
|
6. User scrolls the page to the bottom
|
|
- CmdPal calls `GetMore` on the ListPage, to let it know it should start
|
|
fetching more results
|
|
7. The extension raises a `ItemsChanged(40)`, to indicate that it now has 40 items
|
|
- CmdPal calls `GetItems`, which the command returns the whole list of items
|
|
- CmdPal does an in-place update of the existing items - ignoring the
|
|
unchanged ones, and appending the new ones to the end of the list
|
|
- The extension probably also raises a `PropChanged("HasMoreItems")` here,
|
|
(but we'll skip that for simplicity)
|
|
8. The user types `foo`.
|
|
- CmdPal calls `page.SearchText("is:issue is:open foo")`, to let the
|
|
extension know the query has changed
|
|
9. The extension does the background query to match
|
|
10. The extension raises an `ItemsChanged(5)`, to indicate there are 5 results
|
|
11. CmdPal calls `GetItems` to fetch the items.
|
|
|
|
##### Empty content
|
|
|
|
Developers can specify an `EmptyState` to customize the way the list page looks
|
|
when there are no search results. This will control what's displayed to the user
|
|
when both:
|
|
* `IsLoading = false`
|
|
* `GetItems()` returns null or an empty list
|
|
|
|
This property is observable like anything else, which allows developers to
|
|
change the empty state contextually. For example, consider a "search winget"
|
|
extension:
|
|
|
|
* If the user hasn't typed anything yet -> `Title="Start typing to search winget"`
|
|
* If the user has typed and no results were found -> `Title="No results found"`,
|
|
`Subtitle="Your search '...' returned no packages on winget"`
|
|
|
|
##### Filtering the list
|
|
|
|
Lists are able to specify a set of filters that the user can use to filter or
|
|
pivot the list of results. These are wholly controlled by the extension. To
|
|
indicate that a list page supports filtering, it should set the `Filters`
|
|
property on the list page to a non-null value.
|
|
|
|
```c#
|
|
[uuid("ef5db50c-d26b-4aee-9343-9f98739ab411")]
|
|
interface IFilterItem {}
|
|
|
|
[uuid("0a923c7f-5b7b-431d-9898-3c8c841d02ed")]
|
|
interface ISeparatorFilterItem requires IFilterItem {}
|
|
|
|
interface IFilter requires IFilterItem {
|
|
String Id { get; };
|
|
String Name { get; };
|
|
IIconInfo Icon { get; };
|
|
}
|
|
|
|
interface IFilters {
|
|
String CurrentFilterId { get; set; };
|
|
IFilterItem[] Filters();
|
|
}
|
|
```
|
|
|
|
The extension specifies the text for these filters, and gives them each a unique
|
|
ID. It is also able to specify what the "default" filter is, with whatever the
|
|
initial value of `CurrentFilterId` is.
|
|
|
|
When DevPal calls `GetItems`, the extension can filter the list of items based
|
|
on the selected filter. If the user changes the filter in the UI, DevPal will
|
|
set `CurrentFilterId` to the ID of the selected filter, and call `GetItems`
|
|
again.
|
|
|
|
For example:
|
|
|
|
* the GitHub extension might provide filters for "Issues", "Pull Requests", and
|
|
"Repositories". When the user selects "Issues", the GitHub extension will only
|
|
return issues in the list of items.
|
|
* The Spotify extension may want to provide filters for "Playlists", "Artists",
|
|
"Albums", 'Podcasts'. When the user selects "Artists", the Spotify extension
|
|
will only return artists in the list of items.
|
|
|
|
<!-- ##### Special considerations for Top-level `IListItem`s
|
|
|
|
For the most part, the ListItem's that are returned through `ICommandProvider`'s
|
|
`TopLevelCommands()` call are treated just like any other list items. There are
|
|
a couple caveats, however:
|
|
|
|
* `Tags` will not be displayed.
|
|
* `Details` will not be auto-expanded (if the provider is `Frozen=false`)
|
|
* The `Section` tag is always ignored.
|
|
* `TextToSuggest` is always ignored
|
|
|
|
For command providers that are `Frozen = true`, there are additional caveats
|
|
applied to top-level `IListItem`s:
|
|
|
|
* We won't display any context menu commands for these entries
|
|
* We won't display the Details for these entries (nor the context action to show
|
|
details)
|
|
* Icons which are a `IRandomAccessStream` will not work as expected.
|
|
* If you create a top-level `IListItem` that implements `IFallbackHandler`,
|
|
DevPal will treat your `ICommandProvider` as fresh, never frozen, regardless
|
|
of the value of `Frozen` you set.
|
|
|
|
Again: this only applies to the top-level list items of frozen command
|
|
providers. Once the command provider is instantiated and running, `IListItem`s
|
|
on nested pages will all work exactly as expected. -->
|
|
|
|
#### Content Pages
|
|
|
|
Content pages are used for extensions that want to display richer content than
|
|
just a list of commands to the user. These pages are useful for displaying
|
|
things like documents and forms. You can mix and match different types of
|
|
content on a single page, and even nest content within other content.
|
|
|
|
```csharp
|
|
[uuid("b64def0f-8911-4afa-8f8f-042bd778d088")]
|
|
interface IContent requires INotifyPropChanged {
|
|
}
|
|
|
|
interface IFormContent requires IContent {
|
|
String TemplateJson { get; };
|
|
String DataJson { get; };
|
|
String StateJson { get; };
|
|
ICommandResult SubmitForm(String inputs, String data);
|
|
}
|
|
|
|
interface IMarkdownContent requires IContent {
|
|
String Body { get; };
|
|
}
|
|
|
|
interface ITreeContent requires IContent, INotifyItemsChanged {
|
|
IContent RootContent { get; };
|
|
IContent[] GetChildren();
|
|
}
|
|
|
|
interface IContentPage requires IPage, INotifyItemsChanged {
|
|
IContent[] GetContent();
|
|
IDetails Details { get; };
|
|
IContextItem[] Commands { get; };
|
|
}
|
|
```
|
|
|
|
Content pages may also have a `Details` property, which will be displayed in
|
|
the same way as the details for a list item. This is useful for showing
|
|
additional information about the page, like a description, a preview of a file,
|
|
or a link to more information.
|
|
|
|
Similar to the `List` page, the `Commands` property is a list of commands that the
|
|
user can take on the page. These are the commands that will be shown in the "More
|
|
actions" flyout. Unlike the `List` page, the `Commands` property is not
|
|
associated with any specific item on the page, rather, these commands are global
|
|
to the page itself.
|
|
|
|
##### Markdown Content
|
|
|
|
This is a block of content that displays text formatted with Markdown. This is
|
|
useful for showing a lot of information in a small space. Markdown provides a
|
|
rich set of simple formatting options.
|
|
|
|

|
|
|
|
|
|
An example markdown page for an issue on GitHub:
|
|
|
|
```cs
|
|
class GitHubIssue {
|
|
string Title;
|
|
string Url;
|
|
string Body;
|
|
string Author;
|
|
string[] Tags;
|
|
string[] AssignedTo;
|
|
}
|
|
class GithubIssuePage: Microsoft.CommandPalette.Extensions.Toolkit.ContentPage {
|
|
private readonly MarkdownContent issueBody;
|
|
public GithubIssuePage(GithubIssue issue)
|
|
{
|
|
Commands = [ new CommandContextItem(new Microsoft.CommandPalette.Extensions.Toolkit.OpenUrlCommand(issue.Url)) ];
|
|
Details = new Details(){
|
|
Title = "",
|
|
Body = "",
|
|
Metadata = [
|
|
new Microsoft.CommandPalette.Extensions.Toolkit.DetailsTags(){
|
|
Key = "Author",
|
|
Tags = [new Tag(issue.Author)]
|
|
},
|
|
new Microsoft.CommandPalette.Extensions.Toolkit.DetailsTags(){
|
|
Key = "Assigned To",
|
|
Tags = issue.AssignedTo.Select((user) => new Tag(user)).ToArray()
|
|
},
|
|
new Microsoft.CommandPalette.Extensions.Toolkit.DetailsTags(){
|
|
Key = "Tags",
|
|
Tags = issue.Tags.Select((tag) => new Tag(tag)).ToArray()
|
|
}
|
|
]
|
|
};
|
|
|
|
issueBody = new MarkdownContent(issue.Body);
|
|
}
|
|
|
|
public override IContent[] GetContent() => [issueBody];
|
|
}
|
|
```
|
|
|
|
> [!NOTE]
|
|
> A real GitHub extension would likely load the issue body asynchronously. In
|
|
> that case, the page could start a background thread to fetch the content, then
|
|
> raise the ItemsChanged to signal the host to retrieve the new `IContent`.
|
|
|
|
##### Form Content
|
|
|
|
Forms allow the user to input data to the extension. This is useful for
|
|
actions that might require additional information from the user. For example:
|
|
imagine a "Send Teams message" action. This action might require the user to
|
|
input the message they want to send, and give the user a dropdown to pick the
|
|
chat to send the message to.
|
|
|
|

|
|
|
|
Form content is powered by [Adaptive Cards](https://adaptivecards.io/). This
|
|
allows extension developers a rich set of controls to use in their forms. Each
|
|
page can have as many forms as it needs. These forms will be displayed to the
|
|
user as separate "cards", in the order they are returned by the extension.
|
|
|
|
The `TemplateJson`, `DataJson`, and `StateJson` properties should be a JSON
|
|
string that represents the Adaptive Card to be displayed to the user.
|
|
|
|
When the user submits the form, the `SubmitForm` method will be called with the
|
|
JSON payload of the form. The extension is responsible for parsing this payload
|
|
and acting on it.
|
|
|
|
|
|
### Other types
|
|
|
|
The following are additional type definitions that are used throughout the SDK.
|
|
|
|
#### `ContextItem`s
|
|
|
|
This represents a collection of items that might appear in the `MoreCommands`
|
|
flyout. Mostly, these are just commands and separators.
|
|
|
|
If an `ICommandContextItem` has `MoreCommands`, then when it's invoked, we'll
|
|
create a sub-menu with those items in it.
|
|
|
|
#### Icons - `IconInfo` and `IconData`
|
|
|
|
`IconData` is a wrapper type for passing information about an icon to
|
|
DevPal. This allows extensions to specify apps in a variety of ways, including:
|
|
|
|
* A URL to an image on the web or filesystem
|
|
* A string for an emoji or Segoe Fluent icon
|
|
* A path to an exe, dll or lnk file, to extract the icon from
|
|
* A `IRandomAccessStreamReference` to raw image data. This would be for
|
|
extensions that want to pass us raw image data, which isn't necessarily a file
|
|
which DevPal can load itself.
|
|
|
|
When specifying icons, elements can specify both the light theme and dark theme
|
|
versions of an icon with `IconInfo`.
|
|
|
|
<!-- In .CS because it's manually added to the idl -->
|
|
```cs
|
|
struct IconData {
|
|
IconData(String iconString);
|
|
static IconData FromStream(Windows.Storage.Streams.IRandomAccessStreamReference stream);
|
|
|
|
String Icon { get; };
|
|
Windows.Storage.Streams.IRandomAccessStreamReference Data { get; };
|
|
}
|
|
struct IIconInfo {
|
|
IIconInfo(String iconString);
|
|
IIconInfo(IconData lightIcon, IconData darkIcon);
|
|
|
|
IconData Light { get; };
|
|
IconData Dark { get; };
|
|
}
|
|
```
|
|
|
|
Terminal already has a robust arbitrary string -> icon loader that we can easily
|
|
reuse for this. DevPal will only fall back to the `Data` member if the `Icon`
|
|
member is null or the empty string.
|
|
|
|
As a future consideration, we may also consider supporting a base64 encoded
|
|
image in the `Icon` member. Base64 doesn't include `:`, `.` or `\`, the presence
|
|
of any of which would indicate the string is probably a URI, not base64 data.
|
|
|
|
#### `OptionalColor`
|
|
|
|
We declare our own `Color` struct to avoid depending on `Windows.UI.Color` and
|
|
to avoid passing around unclothed `uint32s`.
|
|
|
|
```c#
|
|
struct Color
|
|
{
|
|
UInt8 R;
|
|
UInt8 G;
|
|
UInt8 B;
|
|
UInt8 A;
|
|
};
|
|
|
|
struct OptionalColor
|
|
{
|
|
Boolean HasValue;
|
|
Microsoft.CommandPalette.Extensions.Color Color;
|
|
};
|
|
```
|
|
|
|
We also define `OptionalColor` as a helper struct here. Yes, this is also just
|
|
an `IReference<Color>`. However, `IReference` has some weird ownership semantics
|
|
that just make it a pain for something as simple as "maybe this color doesn't
|
|
have a value set".
|
|
|
|
#### `Details`
|
|
|
|
This represents additional information that can be displayed about an action or
|
|
item. These can be present on both List pages and Markdown pages. For a List
|
|
page, each element may have its own details. For a Markdown page, the details
|
|
are global to the page.
|
|
|
|
This gif includes a mockup of what the details might look like on a list of
|
|
apps. The details have a title and a hero image, which the action has set to the
|
|
app's icon.
|
|
|
|

|
|
|
|
(However, the buttons in the gif for "Open", "Uninstall", etc, are not part of
|
|
the `Details`, they are part of the "more commands" dropdown. **It's a mockup**)
|
|
|
|
<!-- This block needs to appear in the idl _before_ IListItem, but from a doc
|
|
storytelling standpoint, that doesn't make sense. We've secretly made it a `C#`
|
|
block, and the generator will pull this into the file first. -->
|
|
|
|
```c#
|
|
interface ITag {
|
|
IIconInfo Icon { get; };
|
|
String Text { get; };
|
|
OptionalColor Foreground { get; };
|
|
OptionalColor Background { get; };
|
|
String ToolTip { get; };
|
|
};
|
|
|
|
[uuid("6a6dd345-37a3-4a1e-914d-4f658a4d583d")]
|
|
interface IDetailsData {}
|
|
interface IDetailsElement {
|
|
String Key { get; };
|
|
IDetailsData Data { get; };
|
|
}
|
|
interface IDetails {
|
|
IIconInfo HeroImage { get; };
|
|
String Title { get; };
|
|
String Body { get; };
|
|
IDetailsElement[] Metadata { get; };
|
|
}
|
|
interface IDetailsTags requires IDetailsData {
|
|
ITag[] Tags { get; };
|
|
}
|
|
interface IDetailsLink requires IDetailsData {
|
|
Windows.Foundation.Uri Link { get; };
|
|
String Text { get; };
|
|
}
|
|
interface IDetailsCommand requires IDetailsData {
|
|
ICommand Command { get; };
|
|
}
|
|
[uuid("58070392-02bb-4e89-9beb-47ceb8c3d741")]
|
|
interface IDetailsSeparator requires IDetailsData {}
|
|
```
|
|
|
|
#### `INotifyPropChanged`
|
|
|
|
You may have noticed the presence of the `INotifyPropChanged` interface on
|
|
`ICommand`. Typically this would be a `INotifyPropertyChanged` event from XAML.
|
|
However, we don't want to require that developers use XAML to create extensions.
|
|
So we'll provide a simple `PropertyChanged` event that developers can use to
|
|
notify DevPal that a property has changed.
|
|
|
|
<!-- In .cs because it's manually added to the interface -->
|
|
```cs
|
|
interface INotifyPropChanged {
|
|
event Windows.Foundation.TypedEventHandler<Object, PropChangedEventArgs> PropChanged;
|
|
}
|
|
runtimeclass PropChangedEventArgs {
|
|
String PropName { get; };
|
|
}
|
|
interface INotifyItemsChanged {
|
|
event Windows.Foundation.TypedEventHandler<Object, ItemsChangedEventArgs> ItemsChanged;
|
|
}
|
|
runtimeclass ItemsChangedEventArgs {
|
|
Int32 TotalItems { get; };
|
|
}
|
|
```
|
|
|
|
It's basically exactly the event from XAML. I've named it `PropChanged` to avoid
|
|
prevent confusion with the XAML version.
|
|
|
|
[TODO!api-review]: can we do some trickery in the `idl` to have this PropertyChanged be _literally the same as the XAML one_? So that if there's both in a dll, they get merge into one?
|
|
|
|
#### `ICommandProvider`
|
|
|
|
This is the interface that an extension must implement to provide commands to DevPal.
|
|
|
|
```csharp
|
|
interface ICommandSettings {
|
|
IContentPage SettingsPage { get; };
|
|
};
|
|
|
|
interface IFallbackHandler {
|
|
void UpdateQuery(String query);
|
|
};
|
|
|
|
interface IFallbackCommandItem requires ICommandItem {
|
|
IFallbackHandler FallbackHandler{ get; };
|
|
String DisplayTitle { get; };
|
|
};
|
|
|
|
interface ICommandProvider requires Windows.Foundation.IClosable, INotifyItemsChanged
|
|
{
|
|
String Id { get; };
|
|
String DisplayName { get; };
|
|
IIconInfo Icon { get; };
|
|
ICommandSettings Settings { get; };
|
|
Boolean Frozen { get; };
|
|
|
|
ICommandItem[] TopLevelCommands();
|
|
IFallbackCommandItem[] FallbackCommands();
|
|
|
|
ICommand GetCommand(String id);
|
|
|
|
void InitializeWithHost(IExtensionHost host);
|
|
};
|
|
```
|
|
|
|
`TopLevelCommands` is the method that DevPal will call to get the list of actions
|
|
that should be shown when the user opens DevPal. These are the commands that will
|
|
allow the user to interact with the rest of your extension. They can be simple
|
|
actions, or they can be pages that the user can navigate to.
|
|
|
|
`TopLevelCommands` returns a list of `ICommandItem`s. These are basically just a
|
|
simpler form of `IListItem`, which can be displayed even as a stub (as described
|
|
in [Caching](#caching)), before the extension process is loaded.
|
|
|
|
The `INotifyItemsChanged` interface can be used to let DevPal know at runtime
|
|
that the list of top-level command items has changed. This can be used for
|
|
something like an extension that might require the user to login before
|
|
accessing certain pages within the extension. Command Providers which are
|
|
`Frozen=true` can also use this event to change their list of cached commands,
|
|
since the only time an extension can raise this event is when it's already
|
|
running.
|
|
|
|
`Id` is only necessary to set if your extension implements multiple providers in
|
|
the same package identity. This is an uncommon scenario which most developers
|
|
shouldn't need to worry about. If you do set `Id`, it should be a stable string
|
|
across package versions. DevPal will use this Id for tracking settings for each
|
|
provider within a package. Changing this string will result in the user's
|
|
settings for your extension being lost.
|
|
|
|
#### Fallback commands
|
|
|
|
Providers may also specify a set of `FallbackCommands`[^2]. These are special
|
|
top-level items which allow extensions to have dynamic top-level items which
|
|
respond to the text the user types on the main list page.
|
|
|
|
These are implemented with a special `IFallbackHandler` interface. This is an
|
|
object that will be informed whenever the query changes in List page hosting it.
|
|
This is commonly used for commands that want to allow the user to search for
|
|
something that they've typed that doesn't match any of the commands in the list.
|
|
|
|
For example, if the user types "What's the weather?":
|
|
* the Copilot action might want to be able to update their own action's `Name`
|
|
to be "Ask Copilot 'What's the weather'?".
|
|
* The Store application may want to show a "Search the Store for 'weather'" action.
|
|
* And of course, the SpongeBot extension will want to update its name to "wHaT's
|
|
tHe wEaThEr?".
|
|
|
|
This also gives the action an opportunity to know what the query was before the
|
|
the page is navigated to.
|
|
|
|
Fallback commands will be shown in the top-level search results, even if the
|
|
query doesn't match their `Title`/`Subtitle`, unless their `Title` is empty, in
|
|
which case they won't be shown. This allows for:
|
|
* Fallback items that have dynamic names in response to the search query, but
|
|
not restricted to the query.
|
|
* Fallback items that are hidden until the user types something
|
|
|
|
As an example, here's how a developer might implement a fallback action that
|
|
changes its name to be mOcKiNgCaSe.
|
|
|
|
```cs
|
|
public class SpongebotPage : Microsoft.CommandPalette.Extensions.Toolkit.MarkdownPage, IFallbackHandler
|
|
{
|
|
// Name, Icon, IPropertyChanged: all those are defined in the MarkdownPage base class
|
|
public SpongebotPage()
|
|
{
|
|
this.Name = "";
|
|
this.Icon = new("https://imgflip.com/s/meme/Mocking-Spongebob.jpg");
|
|
}
|
|
public void IFallbackHandler.UpdateQuery(string query) {
|
|
if (string.IsNullOrEmpty(query)) {
|
|
this.Name = "";
|
|
} else {
|
|
this.Name = ConvertToAlternatingCase(query);
|
|
}
|
|
return Task.CompletedTask.AsAsyncCommand();
|
|
}
|
|
static string ConvertToAlternatingCase(string input) {
|
|
StringBuilder sb = new StringBuilder();
|
|
for (var i = 0; i < input.Length; i++)
|
|
{
|
|
sb.Append(i % 2 == 0 ? char.ToUpper(input[i]) : char.ToLower(input[i]));
|
|
}
|
|
return sb.ToString();
|
|
}
|
|
public override string Body() {
|
|
var t = _GenerateMeme(this.Name); // call out to imgflip APIs to generate the meme
|
|
t.ConfigureAwait(false);
|
|
return t.Result;
|
|
}
|
|
}
|
|
internal sealed class SpongebotCommandsProvider : CommandProvider
|
|
{
|
|
public ICommandItem[] TopLevelCommands() => [];
|
|
public IFallbackCommandItem[] FallbackCommands()
|
|
{
|
|
var spongebotPage = new SpongebotPage();
|
|
var listItem = new FallbackCommandItem(spongebotPage);
|
|
// ^ The FallbackCommandItem ctor will automatically set its FallbackHandler to the
|
|
// Command passed in, if the command implements IFallbackHandler
|
|
return [ listItem ];
|
|
}
|
|
}
|
|
```
|
|
|
|
`Microsoft.CommandPalette.Extensions.Toolkit.FallbackCommandItem` in the SDK helpers will automatically set
|
|
the `FallbackHandler` property on the `IFallbackCommandItem` to the `Command` it's
|
|
initialized with, if that command implements `IFallbackHandler`. This allows the
|
|
action to directly update itself in response to the query. You may also specify
|
|
a different `IFallbackHandler`, if needed.
|
|
|
|
We'll include specific affordances within the DevPal settings to allow the user
|
|
to configure which top-level fallbacks are enabled, and in what order. This will
|
|
give the user greater control over the apps that can respond to queries that
|
|
don't match any of the commands in the list.
|
|
|
|
If an extension's own list page wants to implement a similar fallback mechanism
|
|
- it's free to use `IDynamicListPage` to listen for changes to the query and
|
|
have its own ListItem it updates manually.
|
|
|
|
> [!IMPORTANT]
|
|
> If your extension has top-level `FallbackCommandItem`s, then
|
|
> DevPal will treat your `ICommandProvider` as fresh, never frozen, regardless
|
|
of the value of `Frozen` you set.
|
|
|
|
The `DisplayTitle` property allows the user to see a descriptive name for the
|
|
command, without the context of what's been typed. This property is what devpal
|
|
will show to the user in the settings for your application.
|
|
|
|
For example: a "Search the Web" command would just set its `DisplayTitle` to
|
|
"Search the web", but the `UpdateQuery` method might change the title to "Search
|
|
the web for {searchText}"
|
|
|
|
##### `GetCommand`
|
|
|
|
`GetCommand` is a method that DevPal can use to shortcut loading a command, for
|
|
frozen command providers. If a top-level command is assigned an `Id` by the
|
|
extension author, DevPal will use `GetCommand` as a first-resort to instantiate
|
|
that command.
|
|
|
|
For command providers that have multiple top-level commands, this can be a
|
|
helpful short-circuit. The extension won't need to construct instances of all
|
|
the `IListItem`s for all its top-level commands. Instead, the extension can just
|
|
instantiate the requested one.
|
|
|
|
##### Settings
|
|
|
|
Extensions may also want to provide settings to the user. They can do this by
|
|
implementing the `ICommandSettings` interface. This interface has a single
|
|
property, `SettingsPage`, which is a `FormPage`. (We're adding the layer of
|
|
abstraction here to allow for further additions to `ICommandSettings` in the
|
|
future.)
|
|
|
|
In the DevPal settings page, we can then link to each extension's given settings
|
|
page. As these pages are just `FormPage`s, they can be as simple or as complex
|
|
as the extension developer wants, and they're rendered and interacted with in
|
|
the same way.
|
|
|
|
We're then additionally going to provide a collection of
|
|
[settings helpers](#settings-helpers) for developers in the helper SDK. This
|
|
should allow developers to quickly work to add settings, without mucking around
|
|
in building the form JSON themselves.
|
|
|
|
## Helper SDK Classes
|
|
|
|
As a part of the `Microsoft.CommandPalette.Extensions` namespace, we'll provide a set of
|
|
default implementations and helper classes that developers can use to make
|
|
authoring extensions easier.
|
|
|
|
### Default implementations
|
|
|
|
We'll provide default implementations for the following interfaces:
|
|
|
|
* `IInvokableCommand`
|
|
* `IListItem`
|
|
* `ICommandContextItem`
|
|
* `ICommandResult`
|
|
* `IGoToPageArgs`
|
|
* `IChangeQueryArgs`
|
|
* `ISeparatorContextItem`
|
|
* `ISeparatorFilterItem`
|
|
* `IFilter`
|
|
* `IListPage`
|
|
* `IMarkdownPage`
|
|
* `IFormPage`
|
|
* `IDetailsTags`
|
|
* `IDetailsLink`
|
|
* `IDetailsSeparator`
|
|
|
|
This will allow developers to quickly create extensions without having to worry
|
|
about implementing every part of the interface. You can see that reference
|
|
implementation in
|
|
`extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit.Lib\DefaultClasses.cs`.
|
|
|
|
In addition to the default implementations we provide for the interfaces above,
|
|
we should provide a set of helper classes that make it easier for developers to
|
|
write extensions.
|
|
|
|
For example, we should have something like:
|
|
|
|
```cs
|
|
class OpenUrlCommand(string targetUrl, CommandResult result) : Microsoft.CommandPalette.Extensions.Toolkit.InvokableCommand {
|
|
public OpenUrlCommand()
|
|
{
|
|
Name = "Open";
|
|
Icon = new("\uE8A7"); // OpenInNewWindow
|
|
}
|
|
public CommandResult Invoke() {
|
|
Process.Start(new ProcessStartInfo(targetUrl) { UseShellExecute = true });
|
|
return result;
|
|
}
|
|
}
|
|
```
|
|
|
|
Then, the **entire** Hacker News example from above becomes the following. Note
|
|
that no longer do we need to add additional classes for the actions. We just use
|
|
the helper:
|
|
|
|
```cs
|
|
class NewsListItem : Microsoft.CommandPalette.Extensions.Toolkit.ListItem {
|
|
private NewsPost _post;
|
|
public NewsListItem(NewsPost post)
|
|
{
|
|
_post = post;
|
|
Title = post.Title;
|
|
Subtitle = post.Url;
|
|
}
|
|
public IContextItem[] Commands => [
|
|
new CommandContextItem(new OpenUrlCommand(post.Url, CommandResult.KeepOpen)),
|
|
new CommandContextItem(new OpenUrlCommand(post.CommentsUrl, CommandResult.KeepOpen){
|
|
Name = "Open comments",
|
|
Icon = "\uE8F2" // ChatBubbles
|
|
})
|
|
];
|
|
public ITag[] Tags => [ new Tag(){ Text=post.Poster, new Tag(){ Text=post.Points } } ];
|
|
}
|
|
class HackerNewsPage: Microsoft.CommandPalette.Extensions.Toolkit.ListPage {
|
|
public HackerNewsPage()
|
|
{
|
|
Loading = true;
|
|
}
|
|
IListItem[] GetItems(String query) {
|
|
List<NewsItem> items = /* do some RSS feed stuff */;
|
|
this.IsLoading = false;
|
|
return items
|
|
.Select((post) => new NewsListItem(post))
|
|
.ToArray();
|
|
}
|
|
}
|
|
```
|
|
### Using the Clipboard
|
|
|
|
Typically, developers would be expected to use the
|
|
`Clipboard` class from the `Windows.ApplicationModel.DataTransfer` namespace.
|
|
However, this class comes with restrictions around the ability to call it from a
|
|
background thread. Since extensions are always running in the background, this
|
|
presents persistent difficulties.
|
|
|
|
We'll provide a helper class that allows developers to easily use the clipboard
|
|
in their extensions.
|
|
|
|
### Settings helpers
|
|
|
|
The DevPal helpers library also includes a set of helpers for building settings
|
|
pages for you. This lets you define a `Settings` object as a collection of
|
|
properties, controlled by how they're presented in the UI. The helpers library
|
|
will then handle the process of turning those properties into a `IForm` for you.
|
|
|
|
As a complete example: Here's a sample of an app which defines a pair of
|
|
settings (`onOff` and `whatever`) in their `MyAppSettings` class.
|
|
`MyAppSettings` can be responsible for loading or saving the settings however
|
|
the developer best sees fit. They then pass an instance of that object to the
|
|
`MySettingsPage` class they define. In `MySettingsPage.Forms`, the developer
|
|
doesn't need to do any work to build up the Adaptive Card JSON at all. Just call
|
|
`Settings.ToForms()`. The generated form will call back to the extension's code
|
|
in `SettingsChanged` when the user submits the `IForm`. At that point, the
|
|
extension author is again free to do whatever they'd like - store the json
|
|
wherever they want, use the updated values, whatever.
|
|
|
|
```cs
|
|
class MyAppSettings {
|
|
private readonly Helpers.Settings _settings = new();
|
|
public Helpers.Settings Settings => _settings;
|
|
|
|
public MyAppSettings() {
|
|
// Define the structure of your settings here.
|
|
var onOffSetting = new Helpers.ToggleSetting("onOff", "Enable feature", "This feature will do something cool", true);
|
|
var textSetting = new Helpers.TextSetting("whatever", "Text setting", "This is a text setting", "Default text");
|
|
_settings.Add(onOffSetting);
|
|
_settings.Add(onOffSetting);
|
|
}
|
|
public void LoadSavedData()
|
|
{
|
|
// Possibly, load the settings from a file or something
|
|
var persistedData = /* load the settings from file */;
|
|
_settings.LoadState(persistedData);
|
|
}
|
|
public void SaveSettings()
|
|
{
|
|
/* You can save the settings to the file here */
|
|
var mySettingsFilePath = /* whatever */;
|
|
string mySettingsJson = mySettings.Settings.GetState();
|
|
// Or you could raise a event to indicate to the rest of your app that settings have changed.
|
|
}
|
|
}
|
|
|
|
class MySettingsPage : Microsoft.CommandPalette.Extensions.Toolkit.FormPage
|
|
{
|
|
private readonly MyAppSettings mySettings;
|
|
public MySettingsPage(MyAppSettings s) {
|
|
mySettings = s;
|
|
mySettings.Settings.SettingsChanged += SettingsChanged;
|
|
}
|
|
public override IForm[] Forms() {
|
|
// If you haven't already:
|
|
mySettings.Settings.LoadSavedData();
|
|
return mySettings.Settings.ToForms();
|
|
}
|
|
|
|
private void SettingsChanged(object sender, Settings args)
|
|
{
|
|
/* Do something with the new settings here */
|
|
var onOff = _settings.GetSetting<bool>("onOff");
|
|
ExtensionHost.LogMessage(new LogMessage() { Message = $"MySettingsPage: Changed the value of onOff to {onOff}" });
|
|
|
|
// Possibly even:
|
|
mySettings.SaveSettings();
|
|
}
|
|
}
|
|
|
|
// elsewhere in your app:
|
|
|
|
MyAppSettings instance = /* Up to you how you want to pass this around.
|
|
Singleton, dependency injection, whatever. */
|
|
var onOff = instance.Settings.Get("onOff");
|
|
|
|
```
|
|
|
|
## Advanced scenarios
|
|
|
|
### Status messages
|
|
|
|
<img src="./grid-actions-mock.png" height="300px" /> <img src="./grid-status-loading-mock.png" height="300px" /> <img src="./grid-status-success-mock.png" height="300px" />
|
|
|
|
Extensions will want to be able to communicate feedback to the user, based on
|
|
what's going on inside the extension.
|
|
|
|
Consider a `winget` extension that allows the user to search for and install
|
|
packages. When the user starts an install, the extension should be able to show
|
|
a progress bar for the install, and then show a success or error message to the
|
|
user, depending on the result.
|
|
|
|
To do so, extensions can make use of the `IExtensionHost` interface. This is an
|
|
object which extensions shouldn't implement themselves. Rather, this is
|
|
implemented by the host app itself. On this class is the `ShowStatus` &
|
|
`HideStatus` methods.
|
|
|
|
Calling `ShowStatus` will display a given message to the user in the status area
|
|
of the UI. The extension can provide an info/success/warning/error state to
|
|
colorize the message. The extension can also provide progress info in the
|
|
message.
|
|
|
|
When the extension wants to hide the message, they can call `HideStatus`,
|
|
passing the same message object to the method. The host app will keep a log of
|
|
all status's written to it, so a user can alway view old messages once they've
|
|
been hidden. The message also conforms to `INotifyPropChanged`, so extensions
|
|
can modify an existing message if they'd like to. This is recommended, rather
|
|
than writing lots of individual updates as separate messages.
|
|
|
|
```c#
|
|
enum MessageState
|
|
{
|
|
Info = 0,
|
|
Success,
|
|
Warning,
|
|
Error,
|
|
};
|
|
|
|
enum StatusContext
|
|
{
|
|
Page,
|
|
Extension
|
|
};
|
|
|
|
interface IProgressState requires INotifyPropChanged
|
|
{
|
|
Boolean IsIndeterminate { get; };
|
|
UInt32 ProgressPercent { get; };
|
|
};
|
|
|
|
interface IStatusMessage requires INotifyPropChanged
|
|
{
|
|
MessageState State { get; };
|
|
IProgressState Progress { get; };
|
|
String Message { get; };
|
|
// TODO! Icon maybe? Work with design on this
|
|
};
|
|
|
|
interface ILogMessage
|
|
{
|
|
MessageState State { get; };
|
|
String Message { get; };
|
|
};
|
|
|
|
interface IExtensionHost
|
|
{
|
|
Windows.Foundation.IAsyncAction ShowStatus(IStatusMessage message, StatusContext context);
|
|
Windows.Foundation.IAsyncAction HideStatus(IStatusMessage message);
|
|
|
|
Windows.Foundation.IAsyncAction LogMessage(ILogMessage message);
|
|
};
|
|
```
|
|
|
|
There's also a `LogMessage` method provided by the `IExtensionHost` interface.
|
|
This is useful for apps that want to write to the debug log of the host app
|
|
itself. These are for messages that wouldn't otherwise be visible to the user,
|
|
but may be helpful for debugging purposes.
|
|
|
|
`IExtensionHost` is implemented by the hosting app itself. It is passed to a
|
|
command provider on startup, when the host app first connects to the extension.
|
|
For apps which use the helpers library, this will be managed for them. Consumers
|
|
of the helpers lib can simply call `Helpers.ExtensionHost.Host` to get the
|
|
instance from the host app.
|
|
|
|
[TODO!]: I'm marking these methods async right now, to force extension authors
|
|
to remember that these are x-proc calls, and should be treated asynchronously.
|
|
Should the other properties be async too?
|
|
|
|
### Rendering of ICommandItems in Lists and Menus
|
|
|
|
When displaying a list item:
|
|
* The icon is `ICommandItem.Icon ?? ICommandItem.Command.Icon`
|
|
* The title is `ICommandItem.Title ?? ICommandItem.Command.Name`
|
|
* The Subtitle is `ICommandItem.Subtitle`
|
|
* The text displayed for the default action (<kbd>↲</kbd>) is `ICommandItem.Command.Name`
|
|
|
|
When displaying a command context menu item:
|
|
* The icon is `ICommandItem.Icon ?? ICommandItem.Command.Icon`
|
|
* The text is `ICommandItem.Title ?? ICommandItem.Command.Name`
|
|
* The tooltip is `ICommandItem.Subtitle`
|
|
|
|
When displaying a `IListItem`'s default `Command` as a context item, we'll make a new
|
|
```cs
|
|
ICommandContextItem(){
|
|
Command = ICommandItem.Command,
|
|
MoreCommands = null,
|
|
Icon = Command.Icon, // use icon from command, not list item
|
|
Title = Command.Name, // Use command's name, not list item
|
|
Subtitle = IListItem.Title, // Use the title of the list item as the tooltip on the context menu
|
|
IsCritical = false,
|
|
}
|
|
```
|
|
|
|
If a `ICommandItem` in a context menu has `MoreCommands`, then activating it will open a submenu with those items.
|
|
If a `ICommandItem` in a context menu has `MoreCommands` AND a non-null `Command`, then activating it will open a submenu with the `Command` first (following the same rules above for building a context item from a default `Command`), followed by the items in `MoreCommands`.
|
|
|
|
When displaying a page:
|
|
* The title will be `IPage.Title ?? ICommand.Name`
|
|
* The icon will be `ICommand.Icon`
|
|
|
|
|
|
## Class diagram
|
|
|
|
This is a diagram attempting to show the relationships between the various types we've defined for the SDK. Some elements are omitted for clarity. (Notably, `IconData` and `IPropChanged`, which are used in many places.)
|
|
|
|
The notes on the arrows help indicate the multiplicity of the relationship.
|
|
* "*" means 0 or more (for arrays)
|
|
* "?" means 0 or 1 (for optional/nullable properties)
|
|
* "1" means exactly 1 (for required properties)
|
|
|
|
```mermaid
|
|
classDiagram
|
|
class ICommand {
|
|
String Name
|
|
String Id
|
|
IIconInfo Icon
|
|
}
|
|
IPage --|> ICommand
|
|
class IPage {
|
|
String Title
|
|
Boolean IsLoading
|
|
}
|
|
|
|
IInvokableCommand --|> ICommand
|
|
class IInvokableCommand {
|
|
ICommandResult Invoke(object context)
|
|
}
|
|
|
|
class IForm {
|
|
String TemplateJson()
|
|
String DataJson()
|
|
String StateJson()
|
|
ICommandResult SubmitForm(String payload)
|
|
}
|
|
IFormPage --|> IPage
|
|
class IFormPage {
|
|
IForm[] Forms()
|
|
}
|
|
IForm "*" *-- IFormPage
|
|
|
|
IMarkdownPage --|> IPage
|
|
class IMarkdownPage {
|
|
String[] Bodies()
|
|
IDetails Details()
|
|
IContextItem[] Commands
|
|
}
|
|
%% IMarkdownPage *-- IDetails
|
|
IContextItem "*" *-- IMarkdownPage
|
|
IDetails "?" *-- IMarkdownPage
|
|
%%%%%%%%%
|
|
|
|
class IFilterItem
|
|
|
|
ISeparatorFilterItem --|> IFilterItem
|
|
class ISeparatorFilterItem
|
|
|
|
IFilter --|> IFilterItem
|
|
class IFilter {
|
|
String Id
|
|
String Name
|
|
IIconInfo Icon
|
|
}
|
|
|
|
class IFilters {
|
|
String CurrentFilterId
|
|
IFilterItem[] AvailableFilters()
|
|
}
|
|
IFilterItem "*" *-- IFilters
|
|
|
|
class IFallbackHandler {
|
|
void UpdateQuery(String query)
|
|
}
|
|
|
|
|
|
%% IListItem --|> INotifyPropChanged
|
|
class IListItem {
|
|
IIconInfo Icon
|
|
String Title
|
|
String Subtitle
|
|
ICommand Command
|
|
IContextItem[] MoreCommands
|
|
ITag[] Tags
|
|
IDetails Details
|
|
IFallbackHandler FallbackHandler
|
|
String Section
|
|
String TextToSuggest
|
|
}
|
|
IContextItem "*" *-- IListItem
|
|
IDetails "?" *-- IListItem
|
|
ICommand "?" *-- IListItem
|
|
ITag "*" *-- IListItem
|
|
IFallbackHandler "?" *-- IListItem
|
|
|
|
class IGridProperties {
|
|
Windows.Foundation.Size TileSize
|
|
}
|
|
|
|
IListPage --|> IPage
|
|
class IListPage {
|
|
String SearchText
|
|
String PlaceholderText
|
|
Boolean ShowDetails
|
|
IFilters Filters
|
|
IGridProperties GridProperties
|
|
Boolean HasMore
|
|
|
|
IListItem[] GetItems()
|
|
void LoadMore()
|
|
}
|
|
IListItem "*" *-- IListPage
|
|
IFilters "*" *-- IListPage
|
|
IGridProperties "?" *-- IListPage
|
|
|
|
IDynamicListPage --|> IListPage
|
|
class IDynamicListPage {
|
|
+String SearchText set;
|
|
}
|
|
|
|
class IDetails {
|
|
IIconInfo HeroImage
|
|
String Title
|
|
String Body
|
|
IDetailsElement[] Metadata
|
|
}
|
|
|
|
class ITag {
|
|
IIconInfo Icon
|
|
String Text
|
|
Color Color
|
|
String ToolTip
|
|
ICommand Command
|
|
}
|
|
ICommand "?" *-- ITag
|
|
|
|
%%%%%%
|
|
class IContextItem
|
|
|
|
ISeparatorContextItem --|> IContextItem
|
|
class ISeparatorContextItem
|
|
ICommandContextItem --|> IContextItem
|
|
class ICommandContextItem {
|
|
ICommand Command
|
|
String Tooltip
|
|
Boolean IsCritical
|
|
}
|
|
ICommand "?" *-- ICommandContextItem
|
|
|
|
|
|
|
|
class ICommandProvider {
|
|
String DisplayName
|
|
IIconInfo Icon
|
|
Boolean Frozen
|
|
|
|
ICommandItem[] TopLevelCommands()
|
|
IFallbackCommandItem[] FallbackCommands()
|
|
IListItem GetCommand(String id)
|
|
}
|
|
ICommandItem "*" *-- ICommandProvider
|
|
IFallbackCommandItem "*" *-- ICommandProvider
|
|
```
|
|
|
|
|
|
## Future considerations
|
|
|
|
### Arbitrary parameters and arguments
|
|
|
|
Something we'll want to consider soon is how to allow for arbitrary parameters
|
|
to be passed to commands. This allows for commands to require additional info from
|
|
the user _before_ they are run. In its simplest form, this is a lightweight way
|
|
to have an action accept form data inline with the query. But this also allows
|
|
for highly complex action chaining.
|
|
|
|
I had originally started to spec this out as:
|
|
|
|
```cs
|
|
interface IInvokableCommandWithParameters requires ICommand {
|
|
ActionParameters Parameters { get; };
|
|
CommandResult InvokeWithArgs(ActionArguments args);
|
|
}
|
|
```
|
|
|
|
And `ActionParameters` would be a set of `{ type, name, required}` structs,
|
|
which would specify the parameters that the action needs. Simple types would be
|
|
`string`, `file`, `file[]`, `enum` (with possible values), etc.
|
|
|
|
But that may not be complex enough. We recently learned about Action Framework
|
|
and some of their plans there - that may be a good fit for this. My raw notes
|
|
follow - these are not part of the current SDK spec.
|
|
|
|
> [!NOTE]
|
|
>
|
|
> A thought: what if a action returns a `CommandResult.Entity`, then that takes
|
|
> devpal back home, but leaves the entity in the query box. This would allow for
|
|
> a Quicksilver-like "thing, do" flow. That command would prepopulate the
|
|
> parameters. So we would then filter top-level commands based on things that can
|
|
> accept the entity in the search box.
|
|
>
|
|
> For example: The user uses the "Search for file" list page. They find the file
|
|
> they're looking for. That file's ListItem has a context item "With
|
|
> {filename}..." that then returns a `CommandResult.Entity` with the file entity.
|
|
> The user is taken back to the main page, and a file picker badge (with that
|
|
> filename) is at the top of the search box. In that state, the only commands
|
|
> now shown are ones that can accept a File entity. This could be things like
|
|
> the "Remove background" action (from REDACTED), the "Open with" action, the
|
|
> "Send to Teams chat" (which would then ask for another entity). If they did
|
|
> the "Remove Background" one, that could then return _another_ entity.
|
|
>
|
|
> We'd need to solve for the REDACTED case specifically, cause I bet they want to
|
|
> stay in the REDACTED action page, rather than the main one.
|
|
>
|
|
> We'd also probably want the REDACTED one to be able to accept arbitrary
|
|
> entities... like, they probably want a `+` button that lets you add... any
|
|
> kind of entity to their page, rather than explicitly ask for a list of args.
|
|
|
|
However, we do not have enough visibility on how action framework actually
|
|
works, consumer-wise, to be able to specify more. As absolutely fun as chaining
|
|
actions together sounds, I've decided to leave this out of the official v1 spec.
|
|
We can ship a viable v0.1 of DevPal without it, and add it in post.
|
|
|
|
### URI activation
|
|
|
|
We should also consider how to allow for URI activation. This would allow for
|
|
extensions to be activated by a URI, rather than by the user typing a query.
|
|
Consider another app that may want to boot into a particular action page in
|
|
DevPal.
|
|
|
|
I'd imagine that we'd surface some URI scheme that would look like:
|
|
|
|
```
|
|
devpal://commands/{{extension_id}}/{{page_id}}?param=arg
|
|
```
|
|
|
|
that could be used to activate a particular page in an app. Apps would need to
|
|
be able to specify their own extension ID (all my homies dislike working with
|
|
PFNs). DevPal would also need to be able to control which schemes are active,
|
|
and if they're active, which apps can call through to it (if that's knowable).
|
|
|
|
This remains under-specified for now. It's a cool feature for the future, but
|
|
one that's not needed for v1.
|
|
|
|
### Custom "empty list" messages
|
|
|
|
We should consider how to allow for extensions to specify a custom element to be
|
|
shown to the user when the page stops loading and the list of elements filtered
|
|
is empty.
|
|
Is that just a `Details` object? A markdown body?
|
|
|
|
|
|
## Footnotes
|
|
|
|
### Generating the `.idl`
|
|
|
|
The `.idl` for this SDK can be generated directly from this file. To do so, run the following command:
|
|
|
|
```ps1
|
|
.\generate-interface.ps1 > .\Microsoft.DevPalette.Extensions.idl
|
|
```
|
|
|
|
(After a `pip3 install mistletoe`)
|
|
|
|
Or, to generate straight to the place I'm consuming it from:
|
|
|
|
```ps1
|
|
.\doc\initial-sdk-spec\generate-interface.ps1 > .\extensionsdk\Microsoft.CommandPalette.Extensions\Microsoft.CommandPalette.Extensions.Toolkit.idl
|
|
```
|
|
|
|
### Adding APIs
|
|
|
|
Almost all of the SDK defined here is in terms of interfaces. Unfortunately,
|
|
this prevents us from being able to use `[contract]` attributes to add to the
|
|
interfaces. We'll instead need to rely on the tried-and-true method of adding a
|
|
`IFoo2` when we want to add methods to `IFoo`.
|
|
|
|
[^1]: In this example, as in other places, I've referenced a
|
|
`Microsoft.DevPal.Extensions.InvokableCommand` class, as the base for that action.
|
|
Our SDK will include partial class implementations for interfaces like
|
|
`ICommand`, `IListPage`, etc. These partial implementations will provide
|
|
default implementations for common properties and methods, like
|
|
`PropertyChanged` events, and `Icon` properties. Developers can choose to
|
|
use these partial implementations, or implement the interfaces themselves.
|
|
See the [Default implementations](#default-implementations) section for more
|
|
information.
|
|
|
|
[^2]: It is a little weird that `IListItem` has a `FallbackHandler` property,
|
|
rather than just allowing `ICommands` to implement `IFallbackHandler`. This
|
|
is unfortunately due to a quirk in Metadata-based marshalling (MBM) in
|
|
WinRT. MBM doesn't clearly expose when a runtimeclass implements multiple
|
|
non-derived interfaces. Something that implements `IMarkdownPage` and
|
|
`IFallbackHandler` can't be trivially casted from one type to the other.
|
|
However, by making the `FallbackHandler` a property of the ListItem itself,
|
|
then the extension itself can cast between the types, without the need for
|
|
MBM. Thanks to Mano for helping me figure this one out.
|
|
|
|
[^3]: You know, I bet we could make this a frozen provider too. I be if we added
|
|
`INotifyItemsChanged` to `ICommandProvider`, then when the "add bookmark"
|
|
command is activated, we could raise `provider.ItemsChanged`, and have the
|
|
palette re-cache the results of `TopLevelItems`.
|
|
|
|
[Dev Home Extension]: https://learn.microsoft.com/en-us/windows/dev-home/extensions
|
|
[`ISupportIncrementalLoading`]: https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.data.isupportincrementalloading
|