terminal/doc/specs/#1571 - New Tab Menu Customization/#1571 - New Tab Menu Customization.md

301 lines
12 KiB
Markdown
Raw Normal View History

---
author: Mike Griese @zadjii-msft
created on: 2020-5-13
last updated: 2020-08-04
issue id: 1571
---
# New Tab Menu Customization
## Abstract
Many users have lots and _lots_ of profiles that they use. Some of these
profiles the user might not use that frequently. When that happens, the new tab
dropdown can become quite cluttered.
A common ask is for the ability to reorder and reorganize this dropdown. This
spec provides a design for how the user might be able to specify the
customization in their settings.
## Inspiration
Largely, this spec was inspired by discussion in
[#1571](https://github.com/microsoft/terminal/issues/1571#issuecomment-519504048)
and the _many_ linked threads.
## Solution Design
This design proposes adding a new setting `"newTabMenu"`. When unset, (the
default), the new tab menu is populated with all the profiles, in the order they
appear in the users settings file. When set, this enables the user to control
the appearance of the new tab dropdown. Let's take a look at an example:
```json
{
"profiles":{ ... },
"newTabMenu": [
{ "type":"profile", "profile": "cmd" },
{ "type":"profile", "profile": "Windows PowerShell" },
{ "type":"separator" },
{
"type":"folder",
"name": "ssh",
"icon": "C:\\path\\to\\icon.png",
"entries":[
{ "type":"profile", "profile": "Host 1" },
{ "type":"profile", "profile": "8.8.8.8" },
{ "type":"profile", "profile": "Host 2" }
]
},
{ "type":"separator" },
{ "type":"profile", "profile": "Ubuntu-18.04" },
{ "type":"profile", "profile": "Fedora" }
]
}
```
If a user were to use this as their new tab menu, that they would get is a menu
that looks like this:
![fig 1](Menu-Customization-000.png)
_fig 1_: A _very rough_ mockup of what this feature might look like
There are five `type`s of objects in this menu:
* `"type":"profile"`: This is a profile. Clicking on this entry will open a new
tab, with that profile. The profile is identified with the `"profile"`
parameter, which accepts either a profile `name` or GUID. The icon for this
entry will be the profile's icon, and the text on the entry will be the
profile's name.
* `"type":"separator"`: This represents a XAML `MenuFlyoutSeparator`, enabling
the user to visually space out entries.
* `"type":"folder"`: This represents a nested menu of entries.
- The `"name"` property provides a string of text to display for the group.
- The `"icon"` property provides a path to a image to use as the icon. This
property is optional.
- The `"entries"` property specifies a list of menu entries that will appear
nested under this entry. This can contain other `"type":"folder"` groups as
well!
* `"type":"action"`: This represents a menu entry that should execute a specific
`ShortcutAction`.
- the `id` property will specify the global action ID (see [#6899], [#7175])
to identify the action to perform when the user selects the entry. Actions
with invalid IDs will be ignored and omitted from the list.
- The text for this entry will be the action's label (which is
either provided as the `"name"` in the global list of actions, or the
generated name if no `name` was provided)
- The icon for this entry will similarly re-use the action's `icon`.
* `"type":"remainingProfiles"`: This is a special type of entry that will be
expanded to contain one `"type":"profile"` entry for every profile that was
not already listed in the menu. This will allow users to add one entry for
just "all the profiles they haven't manually added to the menu".
- This type of entry can only be specified once - trying to add it to the menu
twice will raise a warning, and ignore all but the first `remainingProfiles`
entry.
- This type of entry can also be set inside a `folder` entry, allowing users
to highlight only a couple profiles in the top-level of the menu, but
enabling all other profiles to also be accessible.
- The "name" of these entries will simply be the name of the profile
- The "icon" of these entries will simply be the profile's icon
The "default" new tab menu could be imagined as the following blob of json:
```json
{
"newTabMenu": [
{ "type":"remainingProfiles" }
]
}
```
### Other considerations
Also considered during the investigation for this feature was re-using the list
of profiles to expose the structure of the new tab menu. For example, doing
something like:
```json
"profiles": {
"defaults": {},
"list":
[
{ "name": "cmd" },
{ "name": "powershell" },
{ "type": "separator" },
{
"type": "folder" ,
"profiles": [
{ "name": "ubuntu" }
]
}
]
}
```
This option was not pursued because we felt that it needlessly complicated the
contents of the list of profiles objects. We'd rather have the `profiles` list
exclusively contain `Profile` objects, and have other elements of the json
_refer_ to those profiles. What if someone would like to have an action that
opened a new tab with profile index 4, and then they set that action as entry 4
in the profile's list? That would certainly be some sort of unexpected behavior.
Additionally, what if someone wants to have an entry that opens a tab with one
pane with one profile in it, and another pane with different profile in it? Or
what if they want the same profile to appear twice in the menu?
By overloading the structure of the `profiles` list, we're forcing all other
consumers of the list of profiles to care about the structure of the elements of
the list. These other consumers should only really care about the list of
profiles, and not necessarily how they're structured in the new tab dropdown.
Furthermore, it complicates the list of profiles, by adding actions intermixed
with the profiles.
The design chosen in this spec more cleanly separates the responsibilities of
the list of profiles and the contents of the new tab menu. This way, each object
can be defined independent of the structure of the other.
## UI/UX Design
See the above _figure 1_.
The profile's `icon` will also appear as the icon on `profile` entries. If
there's a keybinding bound to open a new tab with that profile, then that will
also be added to the `MenuFlyoutItem` as the accelerator text, similar to the
text we have nowadays.
Beneath the list of profiles will _always_ be the same "Settings", "Feedback"
and "About" entries, separated by a `MenuFlyoutSeparator`. This is consistent
with the UI as it exists with no customization. These entries cannot be removed
with this feature, only the list of profiles customized.
## Capabilities
### Accessibility
This menu will be added to the XAML tree in the same fashion as the current new
tab flyout, so there should be no dramatic change here.
### Security
_(no change expected)_
### Reliability
_(no change expected)_
### Compatibility
_(no change expected)_
### Performance, Power, and Efficiency
_(no change expected)_
## Potential Issues
Currently, the `openTab` and `splitPane` keybindings will accept a `index`
parameter to say either:
* "Create a new tab/pane with the N'th profile"
* "Create a new tab/pane with the profile at index N in the new
tab dropdown".
These two were previously synonymous, as the N'th profile was always the N'th in
the dropdown. However, with this change, we'll be changing the meaning of that
argument to mean explicitly the first option - "Open a tab/pane with the N'th
profile".
A previous version of this spec considered changing the meaning of that
parameter to mean "open the entry at index N", the second option. However, in
[Command Palette, Addendum 1], we found that naming that command would become
unnecessarily complex.
To cover that above scenario, we could consider adding an `index` parameter to
the `openNewTabDropdown` action. If specified, that would open either the N'th
action in the dropdown (ignoring separators), or open the dropdown with the n'th
item selected.
The N'th entry in the menu won't always be a profile: it might be a folder with
more options, or it might be an action (that might not be opening a new tab/pane
at all).
Given all the above scenarios, `openNewTabDropdown` with an `"index":N`
parameter will behave in the following ways. If the Nth top-level entry in the
new tab menu is a:
* `"type":"profile"`: perform the `newTab` or `splitPane` action with that profile.
* `"type":"folder"`: Focus the first element in the sub menu, so the user could
navigate it with the keyboard.
* `"type":"separator"`: Ignore these when counting top-level entries.
* `"type":"action"`: Perform the action.
So for example:
```
New Tab Button ▽
├─ Folder 1
│ └─ Profile A
│ └─ Action B
├─ Separator
├─ Folder 2
│ └─ Profile C
│ └─ Profile D
├─ Action E
└─ Profile F
```
And assuming the user has bound:
```json
{
"bindings":
[
{ "command": { "action": "openNewTabDropdown", "index": 0 }, "keys": "ctrl+shift+1" },
{ "command": { "action": "openNewTabDropdown", "index": 1 }, "keys": "ctrl+shift+2" },
{ "command": { "action": "openNewTabDropdown", "index": 2 }, "keys": "ctrl+shift+3" },
{ "command": { "action": "openNewTabDropdown", "index": 3 }, "keys": "ctrl+shift+4" },
]
}
```
* <kbd>ctrl+shift+1</kbd> focuses "Profile A", but the user needs to press
enter/space to creates a new tab/split
* <kbd>ctrl+shift+2</kbd> focuses "Profile C", but the user needs to press
enter/space to creates a new tab/split
* <kbd>ctrl+shift+3</kbd> performs Action E
* <kbd>ctrl+shift+4</kbd> Creates a new tab/split with Profile F
## Future considerations
* The user could set a `"name"`/`"text"`, or `"icon"` property to these menu
items manually, to override the value from the profile or action. These
settings would be totally optional, but it's not unreasonable that someone
might want this.
* We may want to consider adding a default icon for all folders or actions in
the menu. For example, a folder (like 📁) for `folder` entries, or something
like ⚡ for actions. We'll leave these unset by default, and evaluate setting
these icons by default in the future.
* Something considered during review was a way to specify "All my WSL profiles".
Maybe the user wants to have all their profiles generated by the WSL Distro
Generator appear in a "WSL" folder. This would likely require a more elaborate
filtering syntax, to be able to select only profiles where a certain property
has a specific value. Consider the user who has multiple "SSH
me@\<some host\>.com" profiles, and they want all their "SSH\*" profiles to
appear in an "SSH" folder. This feels out-of-scope for this spec.
* A similar structure could potentially also be used for customizing the context
menu within a control, or the context menu for the tab. (see [#3337])
- In both of those cases, it might be important to somehow refer to the
context of the current tab or control in the json. Think for example about
"Close tab" or "Close other tabs" - currently, those work by _knowing_ which
tab the "action" is specified for, not by actually using a `closeTab` action.
In the future, they might need to be implemented as something like
- Close Tab: `{ "action": "closeTab", "index": "${selectedTab.index}" }`
- Close Other Tabs: `{ "action": "closeTabs", "otherThan": "${selectedTab.index}" }`
- Close Tabs to the Right: `{ "action": "closeTabs", "after": "${selectedTab.index}" }`
<!-- Footnotes -->
[#2046]: https://github.com/microsoft/terminal/issues/2046
[Command Palette, Addendum 1]: https://github.com/microsoft/terminal/blob/main/doc/specs/%232046%20-%20Unified%20keybindings%20and%20commands%2C%20and%20synthesized%20action%20names.md
[#3337]: https://github.com/microsoft/terminal/issues/3337
[#6899]: https://github.com/microsoft/terminal/issues/6899
[#7175]: https://github.com/microsoft/terminal/issues/7175