37 KiB
author | created on | last updated | issue id |
---|---|---|---|
Mike Griese @zadjii-msft | 2019-08-01 | 2020-06-16 | 2046 |
Command Palette
Abstract
This spec covers the addition of a "command palette" to the Windows Terminal. The Command Palette is a GUI that the user can activate to search for and execute commands. Beneficially, the command palette allows the user to execute commands even if they aren't bound to a keybinding.
Inspiration
This feature is largely inspired by the "Command Palette" in text editors like VsCode, Sublime Text and others.
This spec was initially drafted in a comment in #2046. That was authored during the annual Microsoft Hackathon, where I proceeded to prototype the solution. This spec is influenced by things I learned prototyping.
Initially, the command palette was designed simply as a method for executing
certain actions that the user pre-defined. With the addition of commandline
arguments to the Windows
Terminal in v0.9, we also considered what it might mean to be able to have the
command palette work as an effective UI not only for dispatching pre-defined
commands, but also wt.exe
commandlines to the current terminal instance.
Solution Design
Fundamentally, we need to address two different modes of using the command palette:
- In the first mode, the command palette can be used to quickly look up pre-defined actions and dispatch them. We'll refer to this as "Action Mode".
- The second mode allows the user to run
wt
commandline commands and have them apply immediately to the current Terminal window. We'll refer to this as "commandline mode".
Both these options will be discussed in detail below.
Action Mode
We'll introduce a new top-level array to the user settings, under the key
commands
. commands
will contain an array of commands, each with the
following schema:
{
"name": string|object,
"action": string|object,
"icon": string
}
Command names should be human-friendly names of actions, though they don't need
to necessarily be related to the action that it fires. For example, a command
with newTab
as the action could have "Open New Tab"
as the name.
The command will be parsed into a new class, Command
:
class Command
{
winrt::hstring Name();
winrt::TerminalApp::ActionAndArgs ActionAndArgs();
winrt::hstring IconSource();
}
We'll add another structure in GlobalAppSettings to hold all these actions. It
will just be a std::vector<Command>
in GlobalAppSettings
.
We'll need app to be able to turn this vector into a ListView
, or similar, so
that we can display this list of actions. Each element in the view will be
intrinsically associated with the Command
object it's associated with. In
order to support this, we'll make Command
a winrt type that implements
Windows.UI.Xaml.Data.INotifyPropertyChanged
. This will let us bind the XAML
element to the winrt type.
When an element is clicked on in the list of commands, we'll raise the event
corresponding to that ShortcutAction
. AppKeyBindings
already does a great
job of dispatching ShortcutActions
(and their associated arguments), so we'll
re-use that. We'll pull the basic parts of dispatching ActionAndArgs
callbacks into another class, ShortcutActionDispatch
, with a single
DoAction(ActionAndArgs)
method (and events for each action).
AppKeyBindings
will be initialized with a reference to the
ShortcutActionDispatch
object, so that it can call DoAction
on it.
Additionally, by having a singular ShortcutActionDispatch
instance, we won't
need to re-hook up the ShortcutAction keybindings each time we re-load the
settings.
In TerminalPage
, when someone clicks on an item in the list, we'll get the
ActionAndArgs
associated with that list element, and call DoAction
on
the app's ShortcutActionDispatch
. This will trigger the event handler just the
same as pressing the keybinding.
Commands for each profile?
#3879 Is a request for being able to launch a profile directly, via the command palette. Essentially, the user will type the name of a profile, and hit enter to launch that profile. I quite like this idea, but with the current spec, this won't work great. We'd need to manually have one entry in the command palette for each profile, and every time the user adds a profile, they'd need to update the list of commands to add a new entry for that profile as well.
This is a fairly complicated addition to this feature, so I'd hold it for "Command Palette v2", though I believe it's solution deserves special consideration from the outset.
I suggest that we need a mechanism by which the user can specify a single command that would be expanded to one command for every profile in the list of profiles. Consider the following sample:
"commands": [
{
"expandOn": "profiles",
"icon": "${profile.icon}",
"name": "New Tab with ${profile.name}",
"command": { "action": "newTab", "profile": "${profile.name}" }
},
{
"expandOn": "profiles",
"icon": "${profile.icon}",
"name": "New Vertical Split with ${profile.name}",
"command": { "action": "splitPane", "split":"vertical", "profile": "${profile.name}" }
}
],
In this example:
- The
"expandOn": "profiles"
property indicates that each command should be repeated for each individual profile. - The
${profile.name}
value is treated as "when expanded, use the given profile's name". This allows each command to use thename
andicon
properties of aProfile
to customize the text of the command.
To ensure that this works correctly, we'll need to make sure to expand these
commands after all the other settings have been parsed, presumably in the
Validate
phase. If we do it earlier, it's possible that not all the profiles
from various sources will have been added yet, which would lead to an incomplete
command list.
We'll need to have a placeholder property to indicate that a command should be
expanded for each Profile
. When the command is first parsed, we'll leave the
format strings ${...}
unexpanded at this time. Then, in the validate phase,
when we encounter a "expandOn": "profiles"
command, we'll remove it from the
list, and use it as a prototype to generate commands for every Profile
in our
profiles list. We'll do a string find-and-replace on the format strings to
replace them with the values from the profile, before adding the completed
command to the list of commands.
Of course, how does this work with localization? Considering the section below, we'd update the built-in commands to the following:
"commands": [
{
"iterateOn": "profiles",
"icon": "${profile.icon}",
"name": { "key": "NewTabWithProfileCommandName" },
"command": { "action": "newTab", "profile": "${profile.name}" }
},
{
"iterateOn": "profiles",
"icon": "${profile.icon}",
"name": { "key": "NewVerticalSplitWithProfileCommandName" },
"command": { "action": "splitPane", "split":"vertical", "profile": "${profile.name}" }
}
],
In this example, we'll look up the NewTabWithProfileCommandName
resource when
we're first parsing the command, to find a string similar to "New Tab with ${profile.name}"
. When we then later expand the command, we'll see the
${profile.name}
bit from the resource, and expand that like we normally would.
Trickily, we'll need to make sure to have a helper for replacing strings like
this that can be used for general purpose arg parsing. As you can see, the
profile
property of the newTab
command also needs the name of the profile.
Either the command validation will need to go through and update these strings
manually, or we'll need another of enabling these IActionArgs
classes to fill
those parameters in based on the profile being used. Perhaps the command
pre-expansion could just stash the json for the action, then expand it later?
This implementation detail is why this particular feature is not slated for
inclusion in an initial Command Palette implementation.
From initial prototyping, it seems like the best solution will be to stash the command's original json around when parsing an expandable command like the above examples. Then, we'll handle the expansion in the settings validation phase, after all the profiles and color schemes have been loaded.
For each profile, we'll need to replace all the instances in the original json
of strings like ${profile.name}
with the profile's name to create a new json
string. We'll attempt to parse that new string into a new command to add to the
list of commands.
Commandline Mode
One of our more highly requested features is the ability to run a wt.exe
commandline in the current WT window (see #4472). Typically, users want the
ability to do this straight from whatever shell they're currently running.
However, we don't really have an effective way currently to know if WT is itself
being called from another WT instance, and passing those arguments to the
hosting WT. Furthermore, in the long term, we see that feature as needing the
ability to not only run commands in the current WT window, but an arbitrary WT
window.
The Command Palette seems like a natural fit for a stopgap measure while we
design the correct way to have a wt
commandline apply to the window it's
running in.
In Commandline Mode, the user can simply type a wt.exe
commandline, and when
they hit enter, we'll parse the commandline and dispatch it to the current
window. So if the user wants to open a new tab, they could type new-tab
in
Commandline Mode, and it would open a new tab in the current window. They're
also free to chain multiple commands like they can with wt
from a shell - by
entering something like split-pane -p "Windows PowerShell" ; split-pane -H wsl.exe
, the terminal would execute two SplitPane
actions in the currently
focused pane, creating one with the "Windows PowerShell" profile and another
with the default profile running wsl
in it.
UI/UX Design
We'll add another action that can be used to toggle the visibility of the command palette. Pressing that keybinding will bring up the command palette. We should make sure to add a argument to this action that specifies whether the palette should be opened directly in Action Mode or Commandline Mode.
When the command palette appears, we'll want it to appear as a single overlay over all of the panes of the Terminal. The drop-down will be centered horizontally, dropping down from the top (from the tab row). When commands are entered, it will be implied that they are delivered to the focused terminal pane. This will help avoid two problematic scenarios that could arise from having the command palette attached to a single pane:
- When attached to a single pane, it might be very easy for the UI to quickly become cluttered, especially at smaller pane sizes.
- This avoids the "find the overlay problem" which is common in editors like VS where the dialog appears attached to the active editor pane.
The palette will consist of two main UI elements: a text box for entering/searching for commands, and in action mode, a list of commands.
Action Mode
The list of commands will be populated with all the commands by default. Each
command will appear like a MenuFlyoutItem
, with an icon at the left (if it has
one) and the name visible. When opened, the palette will automatically highlight
the first entry in the list.
The user can navigate the list of entries with the arrow keys. Hitting enter
will close the palette and execute the action that's highlighted. Hitting escape
will dismiss the palette, returning control to the terminal. When the palette is
closed for any reason (executing a command, dismissing with either escape or the
toggleCommandPalette
keybinding), we'll clear out any search text from the
palette, so the user can start fresh again.
We'll also want to enable the command palette to be filterable, so that the user can type the name of a command, and the command palette will automatically filter the list of commands. This should be more powerful then just a simple string compare - the user should be able to type a search string, and get all the commands that match a "fuzzy search" for that string. This will allow users to find the command they're looking for without needing to type the entire command.
For example, consider the following list of commands:
"commands": [
{ "icon": null, "name": "New Tab", "action": "newTab" },
{ "icon": null, "name": "Close Tab", "action": "closeTab" },
{ "icon": null, "name": "Close Pane", "action": "closePane" },
{ "icon": null, "name": "[-] Split Horizontal", "action": { "action": "splitPane", "split": "horizontal" } },
{ "icon": null, "name": "[ | ] Split Vertical", "action": { "action": "splitPane", "split": "vertical" } },
{ "icon": null, "name": "Next Tab", "action": "nextTab" },
{ "icon": null, "name": "Prev Tab", "action": "prevTab" },
{ "icon": null, "name": "Open Settings", "action": "openSettings" },
{ "icon": null, "name": "Open Media Controls", "action": "openTestPane" }
],
- "open" should return both "Open Settings" and "Open Media Controls".
- "Tab" would return "New Tab", "Close Tab", "Next Tab" and "Prev Tab".
- "P" would return "Close Pane", "[-] Split Horizontal", "[ | ] Split Vertical", "Prev Tab", "Open Settings" and "Open Media Controls".
- Even more powerfully, "sv" would return "[ | ] Split Vertical" (by matching the S in "Split", then the V in "Vertical"). This is a great example of how a user could execute a command with very few keystrokes.
As the user types, we should bold each matching character in the command name, to show how their input correlates to the results on screen.
Additionally, it will be important for commands in the action list to display the keybinding that's bound to them, if there is one.
Commandline Mode
Commandline mode is much simpler. In this mode, we'll simply display a text input,
similar to the search box that's rendered for Action Mode. In this box, the
user will be able to type a wt.exe
style commandline. The user does not need
to start this commandline with wt
(or wtd
, etc) - since we're already
running in WT, the user shouldn't really need to repeat themselves.
When the user hits enter, we'll attempt to parse the commandline. If we're successful in parsing the commandline, we can close the palette and dispatch the commandline. If the commandline had errors, we should reveal a text box with an error message below the text input. We'll leave the palette open with their entered command, so they can edit the commandline and try again. We should probably leave the message up for a few seconds once they've begun editing the commandline, but eventually hide the message (ideally with a motion animation).
Switching Between Modes
TODO: This is a topic for discussion.
How do we differentiate Action Mode from Commandline Mode?
I think there should be a character that the user types that switches the mode.
This is reminiscent of how the command palette works in applications like VsCode
and Sublime Text. The same UI is used for a number of functions. In the case of
VsCode, when the user opens the palette, it's initially in a "navigate to file"
mode. When the user types the prefix character @
, the menu seamlessly switches
to a "navigate to symbol mode". Similarly, users can use :
for "go to line"
and >
enters an "editor command" mode.
I believe we should use a similarly implemented UI. The UI would be in one of the two modes by default, and typing the prefix character would enter the other mode. If the user deletes the prefix character, then we'd switch back into the default mode.
When the user is in Action Mode vs Commandline mode, if the input is empty (besides potentially the prefix character), we should probably have some sort of placeholder text visible to indicate which mode the user is in. Something like "Enter a command name..." for action mode, or "Type a wt commandline..." for commandline mode.
Initially, I favored having the palette in Action Mode by default, and typing a
:
prefix to enter Commandline Mode. This is fairly similar to how tmux's
internal command prompt works, which is bound to <prefix>-:
by default.
If we wanted to remain similar to VsCode, we'd have no prefix character be the
Commandline Mode, and >
would enter the Action mode. I'd think that might
actually be backwards from what I'd expect, with >
being the default
character for the end of the default cmd
%PROMPT%
.
FOR DISCUSSION What option makes the most sense to the team? I'm leaning towards the VsCode style (where Action='>', Commandline='') currently.
Enabling the user to configure this prefix is discussed below in "Future Considerations".
Layering and "Unbinding" Commands
As we'll be providing a list of default commands, the user will inevitably want to change or remove some of these default commands.
Commands should be layered based upon the evaluated value of the "name"
property. Since the default commands will all use localized strings in the
"name": { "key": "KeyName" }
format, the user should be able to override the
command based on the localized string for that command.
So, assuming that NewTabCommandName
is evaluated as "Open New Tab", the
following command
{ "icon": null, "name": { "key": "NewTabCommandName" }, "action": "newTab" },
Could be overridden with the command:
{ "icon": null, "name": "Open New Tab", "action": "splitPane" },
Similarly, if the user wants to remove that command from the command palette,
they could set the action to null
:
{ "icon": null, "name": "Open New Tab", "action": null },
This will remove the command from the command list.
Capabilities
Accessibility
As the entire command palette will be a native XAML element, it'll automatically be hooked up to the UIA tree, allowing for screen readers to naturally find it.
- When the palette is opened, it will automatically receive focus.
- The terminal panes will not be able to be interacted with while the palette is open, which will help keep the UIA tree simple while the palette is open.
Security
This should not introduce any new security concerns. We're relying on the security of jsoncpp for parsing json. Adding new keys to the settings file will rely on jsoncpp's ability to securely parse those json values.
Reliability
We'll need to make sure that invalid commands are ignored. A command could be invalid because:
- it has a null
name
, or a name with the empty string for a value. - it has a null
action
, or an action specified that's not an actualShortcutAction
.
We'll ignore invalid commands from the user's settings, instead of hard crashing. I don't believe this is a scenario that warrants an error dialog to indicate to the user that there's a problem with the json.
Compatibility
We will need to define default commands for all the existing keybinding commands. With #754, we could add all the actions (that make sense) as commands to the commands list, so that everyone wouldn't need to define them manually.
Performance, Power, and Efficiency
We'll be adding a few extra XAML elements to our tree which will certainly increase our runtime memory footprint while the palette is open.
We'll additionally be introducing a few extra json values to parse, so that could increase our load times (though this will likely be negligible).
Potential Issues
This will first require the work in #1205 to work properly. Right now we heavily lean on the "focused" element to determine which terminal is "active". However, when the command palette is opened, focus will move out of the terminal control into the command palette, which leads to some hard to debug crashes.
Additionally, we'll need to ensure that the "fuzzy search" algorithm proposed
above will work for non-english languages, where a single character might be
multiple char
s long. As we'll be using a standard XAML text box for input, we
won't need to worry about handling the input ourselves.
Localization
Because we'll be shipping a set of default commands with the terminal, we should make sure that list of commands can be localizable. Each of the names we'll give to the commands should be locale-specific.
To facilitate this, we'll use a special type of object in JSON that will let us specify a resource name in JSON. We'll use a syntax like the following to suggest that we should load a string from our resources, as opposed to using the value from the file:
"commands": [
{ "icon": null, "name": { "key": "NewTabCommandName" }, "action": "newTab" },
{ "icon": null, "name": { "key": "CloseTabCommandKey" }, "action": "closeTab" },
{ "icon": null, "name": { "key": "ClosePaneCommandKey" }, "action": "closePane" },
{ "icon": null, "name": { "key": "SplitHorizontalCommandKey" }, "action": { "action": "splitPane", "split": "horizontal" } },
{ "icon": null, "name": { "key": "SplitVerticalCommandKey" }, "action": { "action": "splitPane", "split": "vertical" } },
{ "icon": null, "name": { "key": "NextTabCommandKey" }, "action": "nextTab" },
{ "icon": null, "name": { "key": "PrevTabCommandKey" }, "action": "prevTab" },
{ "icon": null, "name": { "key": "OpenSettingsCommandKey" }, "action": "openSettings" },
],
We'll check at parse time if the name
property is a string or an object. If
it's a string, we'll treat that string as the literal text. Otherwise, if it's
an object, we'll attempt to use the key
property of that object to look up a
string from our ResourceDictionary
. This way, we'll be able to ship localized
strings for all the built-in commands, while also allowing the user to easily
add their own commands.
During the spec review process, we considered other options for localization as
well. The original proposal included options such as having one defaults.json
file per-locale, and building the Terminal independently for each locale. Those
were not really feasible options, so we instead settled on this solution, as it
allowed us to leverage the existing localization support provided to us by the
platform.
The { "key": "resourceName" }
solution proposed here was also touched on in
#5280.
Proposed Defaults
These are the following commands I'm proposing adding to the command palette by default. These are largely the actions that are bound by default.
"commands": [
{ "icon": null, "name": { "key": "NewTabCommandKey" }, "action": "newTab" },
{ "icon": null, "name": { "key": "DuplicateTabCommandKey" }, "action": "duplicateTab" },
{ "icon": null, "name": { "key": "DuplicatePaneCommandKey" }, "action": { "action": "splitPane", "split":"auto", "splitMode": "duplicate" } },
{ "icon": null, "name": { "key": "SplitHorizontalCommandKey" }, "action": { "action": "splitPane", "split": "horizontal" } },
{ "icon": null, "name": { "key": "SplitVerticalCommandKey" }, "action": { "action": "splitPane", "split": "vertical" } },
{ "icon": null, "name": { "key": "CloseWindowCommandKey" }, "action": "closeWindow" },
{ "icon": null, "name": { "key": "ClosePaneCommandKey" }, "action": "closePane" },
{ "icon": null, "name": { "key": "OpenNewTabDropdownCommandKey" }, "action": "openNewTabDropdown" },
{ "icon": null, "name": { "key": "OpenSettingsCommandKey" }, "action": "openSettings" },
{ "icon": null, "name": { "key": "FindCommandKey" }, "action": "find" },
{ "icon": null, "name": { "key": "NextTabCommandKey" }, "action": "nextTab" },
{ "icon": null, "name": { "key": "PrevTabCommandKey" }, "action": "prevTab" },
{ "icon": null, "name": { "key": "ToggleFullscreenCommandKey" }, "action": "toggleFullscreen" },
{ "icon": null, "name": { "key": "CopyTextCommandKey" }, "action": { "action": "copy", "singleLine": false } },
{ "icon": null, "name": { "key": "PasteCommandKey" }, "action": "paste" },
{ "icon": null, "name": { "key": "IncreaseFontSizeCommandKey" }, "action": { "action": "adjustFontSize", "delta": 1 } },
{ "icon": null, "name": { "key": "DecreaseFontSizeCommandKey" }, "action": { "action": "adjustFontSize", "delta": -1 } },
{ "icon": null, "name": { "key": "ResetFontSizeCommandKey" }, "action": "resetFontSize" },
{ "icon": null, "name": { "key": "ScrollDownCommandKey" }, "action": "scrollDown" },
{ "icon": null, "name": { "key": "ScrollDownPageCommandKey" }, "action": "scrollDownPage" },
{ "icon": null, "name": { "key": "ScrollUpCommandKey" }, "action": "scrollUp" },
{ "icon": null, "name": { "key": "ScrollUpPageCommandKey" }, "action": "scrollUpPage" }
]
Addenda
This spec also has a follow-up spec which introduces further changes upon this original draft. Please also refer to:
- June 2020: Unified keybindings and commands, and synthesized action names.
Future considerations
- Commands will provide an easy point for allowing an extension to add its actions to the UI, without forcing the user to bind the extension's actions to a keybinding
- Also discussed in #2046 was the potential for adding a command that inputs a
certain commandline to be run by the shell. I felt that was out of scope for
this spec, so I'm not including it in detail. I believe that would be
accomplished by adding a
inputCommand
action, with two args:commandline
, a string, andsuppressNewline
, an optional bool, defaulted to false. TheinputCommand
action would deliver the givencommandline
as input to the connection, followed by a newline (as to execute the command).suppressNewline
would prevent the newline from being added. This would work relatively well, so long as you're sitting at a shell prompt. If you were in an application likevim
, this might be handy for executing a sequence of vim-specific keybindings. Otherwise, you're just going to end up writing a commandline to the buffer of vim. It would be weird, but not unexpected. - Additionally mentioned in #2046 was the potential for profile-scoped commands. While that's a great idea, I believe it's out of scope for this spec.
- Once #754 lands, we'll need to make sure to include commands for each action manually in the default settings. This will add some overhead that the developer will need to do whenever they add an action. That's unfortunate, but will be largely beneficial to the end user.
- We could theoretically also display the keybinding for a certain command in
the
ListViewItem
for the command. We'd need some way to correlate a command's action to a keybinding's action. This could be done in a follow-up task. - We might want to alter the fuzzy-search algorithm, to give higher precedence
in the results list to commands with more consecutive matching characters.
Alternatively we could give more weight to commands where the search matched
the initial character of words in the command.
- For example:
ot
would give more weight to "Open Tab" than "Open Settings").
- For example:
- We may want to add a button to the New Tab Button's dropdown to "Show Command
Palette". I'm hesitant to keep adding new buttons to that UI, but the command
palette is otherwise not highly discoverable.
- We could add another button to the UI to toggle the visibility of the command palette. This was the idea initially proposed in #2046.
- For both these options, we may want a global setting to hide that button, to keep the UI as minimal as possible.
- #1571 is a request for customizing the "new tab dropdown" menu. When we get
to discussing that design, we should consider also enabling users to add
commands from their list of commands to that menu as well.
- This is included in the spec in #5888.
- I think it would be cool if there was a small timeout as the user was typing
in commandline mode before we try to auto-parse their commandline, to check
for errors. Might be useful to help sanity check users. We can always parse
their
wt
commandlines safely without having to execute them. - It would be cool if the commands the user typed in Commandline Mode could be
saved to a history of some sort, so they could easily be re-entered.
- It would be especially cool if it could do this across launches.
- We don't really have any way of storing transient data like that in the Terminal, so that would need to be figured out first.
- Typically the Command Palette is at the top of the view, with the suggestions below it, so navigating through the history would be backwards relative to a normal shell.
- It would be especially cool if it could do this across launches.
- Perhaps users will want the ability to configure which side of the window the
palette appears on?
- This might fit in better with #3327.
- #3753 is a pull request that covers the addition of an "Advanced Tab
Switcher". In an application like VsCode, their advanced tab switcher UI is
similar to their command palette UI. It might make sense that the user could
use the command palette UI to also navigate to active tabs or panes within the
terminal, by control name. We've already outlined how the Command Palette
could operate in "Action Mode" or "Commandline Mode" - we could also add
"Navigate Mode" on
@
, for navigating between tabs or panes.- The tab switcher could probably largely re-use the command palette UI, but maybe hide the input box by default.
- We should make sure to add a setting in the future that lets the user opt-in
to showing most-recently used commands first in the search order, and
possibly even pre-populating the search box with whatever their last entry
was.
- I'm thinking these are two separate settings.
Nested Commands
Another idea for a future spec is the concept of "nested commands", where a single command has many sub-commands. This would hide the children commands from the entire list of commands, allowing for much more succinct top-level list of commands, and allowing related commands to be grouped together.
- For example, I have a text editor plugin that enables rendering markdown to a number of different styles. To use that command in my text editor, first I hit enter on the "Render Markdown..." command, then I select which style I want to render to, in another list of options. This way, I don't need to have three options for "Render Markdown to github", "Render Markdown to gitlab", all in the top-level list.
- We probably also want to allow a nested command set to be evaluated at runtime somehow. Like if we had a "Open New Tab..." command that then had a nested menu with the list of profiles.
The above might be able to be expressed through some JSON like the following:
"commands": [
{
"icon": "...",
"name": { "key": "NewTabWithProfileRootCommandName" },
"commands": [
{
"iterateOn": "profiles",
"icon": "${profile.icon}",
"name": { "key": "NewTabWithProfileCommandName" },
"command": { "action": "newTab", "profile": "${profile.name}" }
}
]
},
{
"icon": "...",
"name": "Connect to ssh...",
"commands": [
{
"icon": "...",
"name": "first.com",
"command": { "action": "newTab", "commandline": "ssh me@first.com" }
},
{
"icon": "...",
"name": "second.com",
"command": { "action": "newTab", "commandline": "ssh me@second.com" }
}
]
}
{
"icon": "...",
"name": { "key": "SplitPaneWithProfileRootCommandName" },
"commands": [
{
"iterateOn": "profiles",
"icon": "${profile.icon}",
"name": { "key": "SplitPaneWithProfileCommandName" },
"commands": [
{
"icon": "...",
"name": { "key": "SplitPaneName" },
"command": { "action": "splitPane", "profile": "${profile.name}", "split": "automatic" }
},
{
"icon": "...",
"name": { "key": "SplitPaneVerticalName" },
"command": { "action": "splitPane", "profile": "${profile.name}", "split": "vertical" }
},
{
"icon": "...",
"name": { "key": "SplitPaneHorizontalName" },
"command": { "action": "splitPane", "profile": "${profile.name}", "split": "horizontal" }
}
]
}
]
}
]
This would define three commands, each with a number of nested commands underneath it:
- For the first command:
- It uses the XAML resource
NewTabWithProfileRootCommandName
as it's name. - Activating this command would cause us to remove all the other commands from the command palette, and only show the nested commands.
- It contains nested commands, one for each profile.
- Each nested command would use the XAML resource
NewTabWithProfileCommandName
, which then would also contain the string${profile.name}
, to be filled with the profile's name in the command's name. - It would also use the profile's icon as the command icon.
- Activating any of the nested commands would dispatch an action to create a new tab with that profile
- Each nested command would use the XAML resource
- It uses the XAML resource
- The second command:
- It uses the string literal
"Connect to ssh..."
as it's name - It contains two nested commands:
- Each nested command has it's own literal name
- Activating these commands would cause us to open a new tab with the
provided
commandline
instead of the default profile'scommandline
- It uses the string literal
- The third command:
- It uses the XAML resource
NewTabWithProfileRootCommandName
as it's name. - It contains nested commands, one for each profile.
- Each one of these sub-commands each contains 3 subcommands - one that will create a new split pane automatically, one vertically, and one horizontally, each using the given profile.
- It uses the XAML resource
So, you could imagine the entire tree as follows:
<Command Palette>
├─ New Tab With Profile...
│ ├─ Profile 1
│ ├─ Profile 2
│ └─ Profile 3
├─ Connect to ssh...
│ ├─ first.com
│ └─ second.com
└─ New Pane...
├─ Profile 1...
| ├─ Split Automatically
| ├─ Split Vertically
| └─ Split Horizontally
├─ Profile 2...
| ├─ Split Automatically
| ├─ Split Vertically
| └─ Split Horizontally
└─ Profile 3...
├─ Split Automatically
├─ Split Vertically
└─ Split Horizontally
Note that the palette isn't displayed like a tree - it only ever displays the commands from one single level at a time. So at first, only:
- New Tab With Profile...
- Connect to ssh...
- New Pane...
are visible. Then, when the user enter's on one of these (like "New Pane"), the UI will change to display:
- Profile 1...
- Profile 2...
- Profile 3...
Configuring the Action/Commandline Mode prefix
As always, I'm also on board with the "this should be configurable by the user" route, so they can change what mode the command palette is in by default, and what the prefixes for different modes are, but I'm not sure how we'd define that cleanly in the settings.
{
"commandPaletteActionModePrefix": "", // or null, for no prefix
"commandPaletteCommandlineModePrefix": ">"
}
We'd need to have validation on that though, what if both of them were set to
null
? One of them would need to be null
, so if both have a character, do
we just assume one is the default?
Resources
Initial post that inspired this spec: #2046
Keybindings args: #1349
Cascading User & Default Settings: #754
Untie "active control" from "currently XAML-focused control" #1205
Allow dropdown menu customization in profiles.json #1571
Search or run a command in Dropdown menu #3879
Spec: Introduce a mini-specification for localized resource use from JSON #5280