23 KiB
author | created on | last updated | issue id |
---|---|---|---|
Mike Griese @zadjii-msft | 2020-06-15 | 2020-06-19 | 2046 |
Command Palette, Addendum 1 - Unified keybindings and commands, and synthesized action names
Abstract
This document is intended to serve as an addition to the Command Palette Spec. While that spec is complete in it's own right, subsequent discussion revealed additional ways to improve the functionality and usability of the command palette. This document builds largely on the topics already introduced in the original spec, so readers should first familiarize themselves with that document.
One point of note from the original document was that the original specification was entirely too verbose when defining both keybindings and commands for actions. Consider, for instance, a user that wants to bind the action "duplicate the current pane". In that spec, they need to add both a keybinding and a command:
{
"keybindings": [
{ "keys": [ "ctrl+alt+t" ], "command": { "action": "splitPane", "split":"auto", "splitMode": "duplicate" } },
],
"commands": [
{ "name": "Duplicate Pane", "action": { "action": "splitPane", "split":"auto", "splitMode": "duplicate" }, "icon": null },
]
}
These two entries are practically the same, except for two key differentiators:
- the keybinding has a
keys
property, indicating which key chord activates the action. - The command has a
name
property, indicating what name to display for the command in the Command Palette.
What if the user didn't have to duplicate this action? What if the user could
just add this action once, in their keybindings
or commands
, and have it
work both as a keybinding AND a command?
Solution Design
This spec will outline two primary changes to keybindings and commands.
- Unify keybindings and commands, so both
keybindings
andcommands
can specify either actions bound to keys, and/or actions bound to entries in the Command Palette. - Propose a mechanism by which actions do not require a
name
to appear in the Command Palette.
These proposals are two atomic units - either could be approved or rejected independently of one another. They're presented together here in the same doc because together, they present a compelling story.
Proposal 1: Unify Keybindings and Commands
As noted above, keybindings and commands have nearly the exact same syntax, save
for a couple properties. To make things easier for the user, I'm proposing
treating everything in both the keybindings
and the commands
arrays as
BOTH a keybinding and a command.
Furthermore, as a change from the previous spec, we'll be using bindings
from
here on as the unified keybindings
and commands
lists. This is considering
that we'll currently be using bindings
for both commands and keybindings, but
we'll potentially also have mouse & touch bindings in this array in the future.
We'll "deprecate" the existing keybindings
property, and begin to exclusively
use bindings
as the new property name. For compatibility reasons, we'll
continue to parse keybindings
in the same way we parse bindings
. We'll
simply layer bindings
on top of the legacy keybindings
.
- Anything entry that has a
keys
value will be added to the keybindings. Pressing that keybinding will activate the action defined incommand
. - Anything with a
name
[1] will be added as an entry (using that name) to the Command Palette's Action Mode.
Caveats
- Nested commands (commands with other nested commands). If a command has
nested commands in the
commands
property, AND akeys
property, then pressing that keybinding should open the Command Palette directly to that level of nesting of commands. - "Iterable" commands (with an
iterateOn
property): These are commands that are expanded into one command per profile. These cannot really be bound as keybindings - which action should be bound to the key? They can't all be bound to the same key. If a KeyBinding/Command json blob has a validiterateOn
property, then we'll ignore it as a keybinding. This includes any commands that are nested as children of this command - we won't be able to know which of the expanded children will be the one to bind the keys to.
[1]: This requirement will be relaxed given Proposal 2, below, but ignored for the remainder of this section, for illustrative purposes.
Example
Consider the following settings:
"bindings": [
{ "name": "Duplicate Tab", "command": "duplicateTab", "keys": "ctrl+alt+a" },
{ "command": "nextTab", "keys": "ctrl+alt+b" },
{
"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": [
{
"keys": "ctrl+alt+c",
"icon": "...",
"name": "first.com",
"command": { "action": "newTab", "commandline": "ssh me@first.com" }
},
{
"keys": "ctrl+alt+d",
"icon": "...",
"name": "second.com",
"command": { "action": "newTab", "commandline": "ssh me@second.com" }
}
]
}
{
"keys": "ctrl+alt+e",
"icon": "...",
"name": { "key": "SplitPaneWithProfileRootCommandName" },
"commands": [
{
"iterateOn": "profiles",
"icon": "${profile.icon}",
"name": { "key": "SplitPaneWithProfileCommandName" },
"commands": [
{
"keys": "ctrl+alt+f",
"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 will generate a tree of commands as follows:
<Command Palette>
├─ Duplicate tab { ctrl+alt+a }
├─ New Tab With Profile...
│ ├─ Profile 1
│ ├─ Profile 2
│ └─ Profile 3
├─ Connect to ssh...
│ ├─ first.com { ctrl+alt+c }
│ └─ second.com { ctrl+alt+d }
└─ New Pane... { ctrl+alt+e }
├─ 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 also the keybindings in the above example:
- ctrl+alt+a: This key chord is bound to the "Duplicate tab"
(
duplicateTab
) action, which is also bound to the command with the same name. - ctrl+alt+b: This key chord is bound to the
nextTab
action, which doesn't have an associated command. - ctrl+alt+c: This key chord is bound to the "Connect to
ssh../first.com" action, which will open a new tab with the
commandline
"ssh me@first.com"
. When the user presses this keybinding, the action will be executed immediately, without the Command Palette appearing. - ctrl+alt+d: This is the same as the above, but with the "Connect to ssh../second.com" action.
- ctrl+alt+e: This key chord is bound to opening the Command Palette
to the "New Pane..." command's menu. When the user presses this keybinding,
they'll be prompted with this command's sub-commands:
Profile 1... Profile 2... Profile 3...
- ctrl+alt+f: This key will not be bound to any action. The parent
action is iterable, which means that the
SplitPaneName
command is going to get turned into one command for each and every profile, and therefore cannot be bound to just a single action.
Proposal 2: Automatically synthesize action names
Previously, all Commands were required to have a name
. This name was used as
the text for the action in the Action Mode of the Command Palette. However, this
is a little tedious for users who already have lots of keys bound. They'll need
to go through and add names to each of their existing keybindings to ensure that
the actions appear in the palette. Could we instead synthesize the names for the
commands ourselves? This would enable users to automatically get each of their
existing keybindings to appear in the palette without any extra work.
To support this, the following changes will be made:
ActionAndArgs
will get aGenerateName()
method defined. This will create a string describing theShortcutAction
and it's associatedActionArgs
.- Not EVERY action needs to define a result for
GenerateName
. Actions that don't won't be automatically added to the Command Palette. - Each of the strings used in
GenerateName
will need to come from our resources, so they can be localized appropriately.
- Not EVERY action needs to define a result for
- When we're parsing commands, if a command doesn't have a
name
, we'll instead attempt to useGenerateName
to create the unique string for the action associated with this command. If the command does have aname
set, we'll use that string instead, allowing the user to override the default name.- If a command has it's name set to
null
, then we'll ignore the command entirely, not just use the generated name.
- If a command has it's name set to
Appendix 1 below
shows a complete sample of the strings that will be generated for each of the existing
ShortcutActions
, and many of the actions that have been proposed, but not yet
implemented.
These strings should be human-friendly versions of the actions and their
associated args. For some of these actions, with very few arguments, the strings
can be relatively simple. Take for example, CopyText
:
JSON | Generated String |
---|---|
{ "action":"copyText" } |
"Copy text" |
{ "action":"copyText", "singleLine": true } |
"Copy text as a single line" |
{ "action":"copyText", "singleLine": false, "copyFormatting": false } |
"Copy text without formatting" |
{ "action":"copyText", "singleLine": true, "copyFormatting": true } |
"Copy text as a single line without formatting" |
CopyText is a bit of a simplistic case however, with very few args or
permutations of argument values. For things like newTab
, splitPane
, where
there are many possible arguments and values, it will be acceptable to simply
append ", property:value"
strings to the generated names for each of the set
values.
For example:
JSON | Generated String |
---|---|
{ "action":"newTab", "profile": "Hello" } |
"Open a new tab, profile:Hello" |
{ "action":"newTab", "profile": "Hello", "directory":"C:\\", "commandline": "wsl.exe", title": "Foo" } |
"Open a new tab, profile:Hello, directory:C:\, commandline:wsl.exe, title:Foo" |
This is being chosen in favor of something that might be more human-friendly, like "Open a new tab with profile {profile name} in {directory} with {commandline} and a title of {title}". This string would be much harder to synthesize, especially considering localization concerns.
Remove the resource key notation
Since we'll be using localized names for each of the actions in GenerateName
,
we no longer need to provide the { "name":{ "key": "SomeResourceKey" } }
syntax introduced in the original spec. This functionality was used to allow us
to define localizable names for the default commands.
However, I think we should keep this functionality, to allow us additional flexibility when defining default commands.
Complete Defaults
Considering both of the above proposals, the default keybindings and commands will be defined as follows:
- The current default keybindings will be untouched. These actions will
automatically be added to the Command Palette, using their names generated
from
GenerateName
.- TODO: FOR DISCUSSION: Should we manually set the names for the default
"New Tab, profile index: 0" keybindings to
null
? This seems like a not terribly helpful name for the Command Palette, especially considering the iterable commands listed below.
- TODO: FOR DISCUSSION: Should we manually set the names for the default
"New Tab, profile index: 0" keybindings to
- We'll add a few new commands:
- A nested, iterable command for "Open new tab with profile..."/"Profile:{profile name}"
- A nested, iterable command for "Select color scheme..."/"{scheme name}"
- A nested, iterable command for "New Pane..."/"Profile:{profile name}..."/["Automatic", "Horizontal", "Vertical"]
👉 NOTE: These default nested commands can be removed by the user defining
{ "name": "Open new tab with profile...", "action":null }
(et al) in their settings.- If we so chose, in the future we can add further commands that we think are
helpful to
defaults.json
, without needing to give them keys. For example, we could add
to{ "command": { "action": "copy", "singleLine": true } }
bindings
, to add a "copy text as a single line" command, without necessarily binding it to a keystroke.
These changes to the defaults.json
are represented in json as the following:
"bindings": [
{
"icon": null,
"name": { "key": "NewTabWithProfileRootCommandName" },
"commands": [
{
"iterateOn": "profiles",
"icon": "${profile.icon}",
"name": "${profile.name}",
"command": { "action": "newTab", "profile": "${profile.name}" }
}
]
},
{
"icon": null,
"name": { "key": "SelectColorSchemeRootCommandName" },
"commands": [
{
"iterateOn": "schemes",
"icon": null,
"name": "${scheme.name}",
"command": { "action": "selectColorScheme", "scheme": "${scheme.name}" }
}
]
},
{
"icon": null,
"name": { "key": "SplitPaneWithProfileRootCommandName" },
"commands": [
{
"iterateOn": "profiles",
"icon": "${profile.icon}",
"name": { "key": "SplitPaneWithProfileCommandName" },
"commands": [
{
"icon": null,
"name": { "key": "SplitPaneName" },
"command": { "action": "splitPane", "profile": "${profile.name}", "split": "automatic" }
},
{
"icon": null,
"name": { "key": "SplitPaneVerticalName" },
"command": { "action": "splitPane", "profile": "${profile.name}", "split": "vertical" }
},
{
"icon": null,
"name": { "key": "SplitPaneHorizontalName" },
"command": { "action": "splitPane", "profile": "${profile.name}", "split": "horizontal" }
}
]
}
]
}
]
A complete diagram of what the default Command Palette will look like given the default keybindings and these changes is given in Appendix 2.
Concerns
DISCUSSION: "New tab with index {index}". How does this play with
the new tab dropdown customizations in [#5888]? In recent iterations of that
spec, we changed the meaning of { "action": "newTab", "index": 1 }
to mean
"open the first entry in the new tab menu". If that's a profile, then we'll open
a new tab with it. If it's an action, we'll perform that action. If it's a
nested menu, then we'll open the menu to that entry.
Additionally, how exactly does that play with something like { "action": "newTab", "index": 1, "commandline": "wsl.exe" }
? This is really a discussion
for that spec, but is an issue highlighted by this spec. If the first entry is
anything other than a profile
, then the commandline
parameter doesn't really
mean anything anymore. I'm tempted to revert this particular portion of the new
tab menu customization spec over this.
We could instead add an index
to openNewTabDropdown
, and have that string
instead be "Open new tab dropdown, index:1". That would help disambiguate the
two.
Following discussion, it was decided that this was in fact the cleanest solution, when accounting for both the needs of the new tab dropdown and the command palette. The [#5888] spec has been updated to reflect this.
Future considerations
- Some of these command names are starting to get very long. Perhaps we need a netting to display Command Palette entries on two lines (or multiple, as necessary).
- When displaying the entries of a nested command to the user, should we display
a small label showing the name of the previous command? My gut says yes. In
the Proposal 1 example, pressing
ctrl+alt+e
to jump to "Split Pane..." should probably show a small label that displays "Split Pane..." above the list of nested commands. - It wouldn't be totally impossible to allow keys to be bound to an iterable command, and then simply have the key work as "open the command palette with only the commands generated by this iterable command". This is left as a future option, as it might require some additional technical plumbing.
Appendix 1: Name generation samples for ShortcutAction
s
Current ShortcutActions
CopyText
- "Copy text"
- "Copy text as a single line"
- "Copy text without formatting"
- "Copy text as a single line without formatting"
PasteText
- "Paste text"
OpenNewTabDropdown
- "Open new tab dropdown"
DuplicateTab
- "Duplicate tab"
NewTab
- "Open a new tab, profile:{profile name}, directory:{directory}, commandline:{commandline}, title:{title}"
NewWindow
- "Open a new window"
- "Open a new window, profile:{profile name}, directory:{directory}, commandline:{commandline}, title:{title}"
CloseWindow
- "Close window"
CloseTab
- "Close tab"
ClosePane
- "Close pane"
NextTab
- "Switch to the next tab"
PrevTab
- "Switch to the previous tab"
SplitPane
- "Open a new pane, profile:{profile name}, split direction:{direction}, split size:{X%/Y chars}, resize parents, directory:{directory}, commandline:{commandline}, title:{title}"
- "Duplicate the current pane, split direction:{direction}, split size:{X%/Y chars}, resize parents, directory:{directory}, commandline:{commandline}, title:{title}"
SwitchToTab
- "Switch to tab {index}"
AdjustFontSize
- "Increase the font size"
- "Decrease the font size"
ResetFontSize
- "Reset the font size"
ScrollUp
- "Scroll up a line"
- "Scroll up {amount} lines"
ScrollDown
- "Scroll down a line"
- "Scroll down {amount} lines"
ScrollUpPage
- "Scroll up a page"
- "Scroll up {amount} pages"
ScrollDownPage
- "Scroll down a page"
- "Scroll down {amount} pages"
ResizePane
- "Resize pane {direction}"
- "Resize pane {direction} {percent}%"
MoveFocus
- "Move focus {direction}"
Find
- "Toggle the search box"
ToggleFullscreen
- "Toggle fullscreen mode"
OpenSettings
- "Open settings"
- "Open settings file"
- "Open default settings file"
ToggleCommandPalette
- "Toggle the Command Palette"
- "Toggle the Command Palette in commandline mode"
Other yet unimplemented actions:
SwitchColorScheme
- "Select color scheme {name}"
ToggleRetroEffect
- "Toggle the retro terminal effect"
ExecuteCommandline
- "Run a wt commandline: {cmdline}"
ExecuteActions
- OPINION: THIS ONE SHOULDN'T HAVE A NAME. We're not including any of these by default. The user knows what they're putting in the settings by adding this action, let them name it.
- Alternatively: "Run actions: {action.ToName() for action in actions}"
SendInput
- OPINION: THIS ONE SHOULDN'T HAVE A NAME. We're not including any of these by default. The user knows what they're putting in the settings by adding this action, let them name it.
ToggleMarkMode
- "Toggle Mark Mode"
NextTab
- "Switch to the next most-recent tab"
SetTabColor
- "Set the color of the current tab to {#color}"
- It would be really cool if we could display a sample of the color inline, but that's left as a future consideration.
- "Set the color for this tab..."
- this command isn't nested, but hitting enter immediately does something with the UI, so that's fine
- "Set the color of the current tab to {#color}"
RenameTab
- "Rename this tab to {name}"
- "Rename this tab..."
- this command isn't nested, but hitting enter immediately does something with the UI, so that's fine
Appendix 2: Complete Default Command Palette
This diagram shows what the default value of the Command Palette would be. This assumes that the user has 3 profiles, "Profile 1", "Profile 2", and "Profile 3", as well as 3 schemes: "Scheme 1", "Scheme 2", and "Scheme 3".
<Command Palette>
├─ Close Window
├─ Toggle fullscreen mode
├─ Open new tab dropdown
├─ Open settings
├─ Open default settings file
├─ Toggle the search box
├─ New Tab
├─ New Tab, profile index: 0
├─ New Tab, profile index: 1
├─ New Tab, profile index: 2
├─ New Tab, profile index: 3
├─ New Tab, profile index: 4
├─ New Tab, profile index: 5
├─ New Tab, profile index: 6
├─ New Tab, profile index: 7
├─ New Tab, profile index: 8
├─ Duplicate tab
├─ Switch to the next tab
├─ Switch to the previous tab
├─ Switch to tab 0
├─ Switch to tab 1
├─ Switch to tab 2
├─ Switch to tab 3
├─ Switch to tab 4
├─ Switch to tab 5
├─ Switch to tab 6
├─ Switch to tab 7
├─ Switch to tab 8
├─ Close pane
├─ Open a new pane, split: horizontal
├─ Open a new pane, split: vertical
├─ Duplicate the current pane
├─ Resize pane down
├─ Resize pane left
├─ Resize pane right
├─ Resize pane up
├─ Move focus down
├─ Move focus left
├─ Move focus right
├─ Move focus up
├─ Copy Text
├─ Paste Text
├─ Scroll down a line
├─ Scroll down a page
├─ Scroll up a line
├─ Scroll up a page
├─ Increase the font size
├─ Decrease the font size
├─ Reset the font size
├─ New Tab With Profile...
│ ├─ Profile 1
│ ├─ Profile 2
│ └─ Profile 3
├─ Select Color Scheme...
│ ├─ Scheme 1
│ ├─ Scheme 2
│ └─ Scheme 3
└─ 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