|
| 1 | +--- |
| 2 | +author: Mike Griese @zadjii-msft |
| 3 | +created on: 2020-5-13 |
| 4 | +last updated: 2020-08-04 |
| 5 | +issue id: 1571 |
| 6 | +--- |
| 7 | + |
| 8 | +# New Tab Menu Customization |
| 9 | + |
| 10 | +## Abstract |
| 11 | + |
| 12 | +Many users have lots and _lots_ of profiles that they use. Some of these |
| 13 | +profiles the user might not use that frequently. When that happens, the new tab |
| 14 | +dropdown can become quite cluttered. |
| 15 | + |
| 16 | +A common ask is for the ability to reorder and reorganize this dropdown. This |
| 17 | +spec provides a design for how the user might be able to specify the |
| 18 | +customization in their settings. |
| 19 | + |
| 20 | +## Inspiration |
| 21 | + |
| 22 | +Largely, this spec was inspired by discussion in |
| 23 | +[#1571](https://github.com/microsoft/terminal/issues/1571#issuecomment-519504048) |
| 24 | +and the _many_ linked threads. |
| 25 | + |
| 26 | +## Solution Design |
| 27 | + |
| 28 | +This design proposes adding a new setting `"newTabMenu"`. When unset, (the |
| 29 | +default), the new tab menu is populated with all the profiles, in the order they |
| 30 | +appear in the users settings file. When set, this enables the user to control |
| 31 | +the appearance of the new tab dropdown. Let's take a look at an example: |
| 32 | + |
| 33 | +```json |
| 34 | +{ |
| 35 | + "profiles":{ ... }, |
| 36 | + "newTabMenu": [ |
| 37 | + { "type":"profile", "profile": "cmd" }, |
| 38 | + { "type":"profile", "profile": "Windows PowerShell" }, |
| 39 | + { "type":"separator" }, |
| 40 | + { |
| 41 | + "type":"folder", |
| 42 | + "name": "ssh", |
| 43 | + "icon": "C:\\path\\to\\icon.png", |
| 44 | + "entries":[ |
| 45 | + { "type":"profile", "profile": "Host 1" }, |
| 46 | + { "type":"profile", "profile": "8.8.8.8" }, |
| 47 | + { "type":"profile", "profile": "Host 2" } |
| 48 | + ] |
| 49 | + }, |
| 50 | + { "type":"separator" }, |
| 51 | + { "type":"profile", "profile": "Ubuntu-18.04" }, |
| 52 | + { "type":"profile", "profile": "Fedora" } |
| 53 | + ] |
| 54 | +} |
| 55 | +``` |
| 56 | + |
| 57 | +If a user were to use this as their new tab menu, that they would get is a menu |
| 58 | +that looks like this: |
| 59 | + |
| 60 | + |
| 61 | + |
| 62 | +_fig 1_: A _very rough_ mockup of what this feature might look like |
| 63 | + |
| 64 | +There are five `type`s of objects in this menu: |
| 65 | +* `"type":"profile"`: This is a profile. Clicking on this entry will open a new |
| 66 | + tab, with that profile. The profile is identified with the `"profile"` |
| 67 | + parameter, which accepts either a profile `name` or GUID. The icon for this |
| 68 | + entry will be the profile's icon, and the text on the entry will be the |
| 69 | + profile's name. |
| 70 | +* `"type":"separator"`: This represents a XAML `MenuFlyoutSeparator`, enabling |
| 71 | + the user to visually space out entries. |
| 72 | +* `"type":"folder"`: This represents a nested menu of entries. |
| 73 | + - The `"name"` property provides a string of text to display for the group. |
| 74 | + - The `"icon"` property provides a path to a image to use as the icon. This |
| 75 | + property is optional. |
| 76 | + - The `"entries"` property specifies a list of menu entries that will appear |
| 77 | + nested under this entry. This can contain other `"type":"folder"` groups as |
| 78 | + well! |
| 79 | +* `"type":"action"`: This represents a menu entry that should execute a specific |
| 80 | + `ShortcutAction`. |
| 81 | + - the `id` property will specify the global action ID (see [#6899], [#7175]) |
| 82 | + to identify the action to perform when the user selects the entry. Actions |
| 83 | + with invalid IDs will be ignored and omitted from the list. |
| 84 | + - The text for this entry will be the action's label (which is |
| 85 | + either provided as the `"name"` in the global list of actions, or the |
| 86 | + generated name if no `name` was provided) |
| 87 | + - The icon for this entry will similarly re-use the action's `icon`. |
| 88 | +* `"type":"remainingProfiles"`: This is a special type of entry that will be |
| 89 | + expanded to contain one `"type":"profile"` entry for every profile that was |
| 90 | + not already listed in the menu. This will allow users to add one entry for |
| 91 | + just "all the profiles they haven't manually added to the menu". |
| 92 | + - This type of entry can only be specified once - trying to add it to the menu |
| 93 | + twice will raise a warning, and ignore all but the first `remainingProfiles` |
| 94 | + entry. |
| 95 | + - This type of entry can also be set inside a `folder` entry, allowing users |
| 96 | + to highlight only a couple profiles in the top-level of the menu, but |
| 97 | + enabling all other profiles to also be accessible. |
| 98 | + - The "name" of these entries will simply be the name of the profile |
| 99 | + - The "icon" of these entries will simply be the profile's icon |
| 100 | + |
| 101 | +The "default" new tab menu could be imagined as the following blob of json: |
| 102 | + |
| 103 | +```json |
| 104 | +{ |
| 105 | + "newTabMenu": [ |
| 106 | + { "type":"remainingProfiles" } |
| 107 | + ] |
| 108 | +} |
| 109 | +``` |
| 110 | + |
| 111 | +### Other considerations |
| 112 | + |
| 113 | +Also considered during the investigation for this feature was re-using the list |
| 114 | +of profiles to expose the structure of the new tab menu. For example, doing |
| 115 | +something like: |
| 116 | + |
| 117 | +```json |
| 118 | +"profiles": { |
| 119 | + "defaults": {}, |
| 120 | + "list": |
| 121 | + [ |
| 122 | + { "name": "cmd" }, |
| 123 | + { "name": "powershell" }, |
| 124 | + { "type": "separator" }, |
| 125 | + { |
| 126 | + "type": "folder" , |
| 127 | + "profiles": [ |
| 128 | + { "name": "ubuntu" } |
| 129 | + ] |
| 130 | + } |
| 131 | + ] |
| 132 | +} |
| 133 | +``` |
| 134 | + |
| 135 | +This option was not pursued because we felt that it needlessly complicated the |
| 136 | +contents of the list of profiles objects. We'd rather have the `profiles` list |
| 137 | +exclusively contain `Profile` objects, and have other elements of the json |
| 138 | +_refer_ to those profiles. What if someone would like to have an action that |
| 139 | +opened a new tab with profile index 4, and then they set that action as entry 4 |
| 140 | +in the profile's list? That would certainly be some sort of unexpected behavior. |
| 141 | + |
| 142 | +Additionally, what if someone wants to have an entry that opens a tab with one |
| 143 | +pane with one profile in it, and another pane with different profile in it? Or |
| 144 | +what if they want the same profile to appear twice in the menu? |
| 145 | + |
| 146 | +By overloading the structure of the `profiles` list, we're forcing all other |
| 147 | +consumers of the list of profiles to care about the structure of the elements of |
| 148 | +the list. These other consumers should only really care about the list of |
| 149 | +profiles, and not necessarily how they're structured in the new tab dropdown. |
| 150 | +Furthermore, it complicates the list of profiles, by adding actions intermixed |
| 151 | +with the profiles. |
| 152 | + |
| 153 | +The design chosen in this spec more cleanly separates the responsibilities of |
| 154 | +the list of profiles and the contents of the new tab menu. This way, each object |
| 155 | +can be defined independent of the structure of the other. |
| 156 | + |
| 157 | +## UI/UX Design |
| 158 | + |
| 159 | +See the above _figure 1_. |
| 160 | + |
| 161 | +The profile's `icon` will also appear as the icon on `profile` entries. If |
| 162 | +there's a keybinding bound to open a new tab with that profile, then that will |
| 163 | +also be added to the `MenuFlyoutItem` as the accelerator text, similar to the |
| 164 | +text we have nowadays. |
| 165 | + |
| 166 | +Beneath the list of profiles will _always_ be the same "Settings", "Feedback" |
| 167 | +and "About" entries, separated by a `MenuFlyoutSeparator`. This is consistent |
| 168 | +with the UI as it exists with no customization. These entries cannot be removed |
| 169 | +with this feature, only the list of profiles customized. |
| 170 | + |
| 171 | +## Capabilities |
| 172 | + |
| 173 | +### Accessibility |
| 174 | + |
| 175 | +This menu will be added to the XAML tree in the same fashion as the current new |
| 176 | +tab flyout, so there should be no dramatic change here. |
| 177 | + |
| 178 | +### Security |
| 179 | + |
| 180 | +_(no change expected)_ |
| 181 | + |
| 182 | +### Reliability |
| 183 | + |
| 184 | +_(no change expected)_ |
| 185 | + |
| 186 | +### Compatibility |
| 187 | + |
| 188 | +_(no change expected)_ |
| 189 | + |
| 190 | +### Performance, Power, and Efficiency |
| 191 | + |
| 192 | +_(no change expected)_ |
| 193 | + |
| 194 | +## Potential Issues |
| 195 | + |
| 196 | +Currently, the `openTab` and `splitPane` keybindings will accept a `index` |
| 197 | +parameter to say either: |
| 198 | +* "Create a new tab/pane with the N'th profile" |
| 199 | +* "Create a new tab/pane with the profile at index N in the new |
| 200 | +tab dropdown". |
| 201 | + |
| 202 | +These two were previously synonymous, as the N'th profile was always the N'th in |
| 203 | +the dropdown. However, with this change, we'll be changing the meaning of that |
| 204 | +argument to mean explicitly the first option - "Open a tab/pane with the N'th |
| 205 | +profile". |
| 206 | + |
| 207 | +A previous version of this spec considered changing the meaning of that |
| 208 | +parameter to mean "open the entry at index N", the second option. However, in |
| 209 | +[Command Palette, Addendum 1], we found that naming that command would become |
| 210 | +unnecessarily complex. |
| 211 | + |
| 212 | +To cover that above scenario, we could consider adding an `index` parameter to |
| 213 | +the `openNewTabDropdown` action. If specified, that would open either the N'th |
| 214 | +action in the dropdown (ignoring separators), or open the dropdown with the n'th |
| 215 | +item selected. |
| 216 | + |
| 217 | +The N'th entry in the menu won't always be a profile: it might be a folder with |
| 218 | +more options, or it might be an action (that might not be opening a new tab/pane |
| 219 | +at all). |
| 220 | + |
| 221 | +Given all the above scenarios, `openNewTabDropdown` with an `"index":N` |
| 222 | +parameter will behave in the following ways. If the Nth top-level entry in the |
| 223 | +new tab menu is a: |
| 224 | +* `"type":"profile"`: perform the `newTab` or `splitPane` action with that profile. |
| 225 | +* `"type":"folder"`: Focus the first element in the sub menu, so the user could |
| 226 | + navigate it with the keyboard. |
| 227 | +* `"type":"separator"`: Ignore these when counting top-level entries. |
| 228 | +* `"type":"action"`: Perform the action. |
| 229 | + |
| 230 | +So for example: |
| 231 | + |
| 232 | +``` |
| 233 | +New Tab Button ▽ |
| 234 | +├─ Folder 1 |
| 235 | +│ └─ Profile A |
| 236 | +│ └─ Action B |
| 237 | +├─ Separator |
| 238 | +├─ Folder 2 |
| 239 | +│ └─ Profile C |
| 240 | +│ └─ Profile D |
| 241 | +├─ Action E |
| 242 | +└─ Profile F |
| 243 | +``` |
| 244 | + |
| 245 | +And assuming the user has bound: |
| 246 | +```json |
| 247 | +{ |
| 248 | + "bindings": |
| 249 | + [ |
| 250 | + { "command": { "action": "openNewTabDropdown", "index": 0 }, "keys": "ctrl+shift+1" }, |
| 251 | + { "command": { "action": "openNewTabDropdown", "index": 1 }, "keys": "ctrl+shift+2" }, |
| 252 | + { "command": { "action": "openNewTabDropdown", "index": 2 }, "keys": "ctrl+shift+3" }, |
| 253 | + { "command": { "action": "openNewTabDropdown", "index": 3 }, "keys": "ctrl+shift+4" }, |
| 254 | + ] |
| 255 | +} |
| 256 | +``` |
| 257 | + |
| 258 | +* <kbd>ctrl+shift+1</kbd> focuses "Profile A", but the user needs to press |
| 259 | + enter/space to creates a new tab/split |
| 260 | +* <kbd>ctrl+shift+2</kbd> focuses "Profile C", but the user needs to press |
| 261 | + enter/space to creates a new tab/split |
| 262 | +* <kbd>ctrl+shift+3</kbd> performs Action E |
| 263 | +* <kbd>ctrl+shift+4</kbd> Creates a new tab/split with Profile F |
| 264 | + |
| 265 | +## Future considerations |
| 266 | + |
| 267 | +* The user could set a `"name"`/`"text"`, or `"icon"` property to these menu |
| 268 | + items manually, to override the value from the profile or action. These |
| 269 | + settings would be totally optional, but it's not unreasonable that someone |
| 270 | + might want this. |
| 271 | +* We may want to consider adding a default icon for all folders or actions in |
| 272 | + the menu. For example, a folder (like 📁) for `folder` entries, or something |
| 273 | + like ⚡ for actions. We'll leave these unset by default, and evaluate setting |
| 274 | + these icons by default in the future. |
| 275 | +* Something considered during review was a way to specify "All my WSL profiles". |
| 276 | + Maybe the user wants to have all their profiles generated by the WSL Distro |
| 277 | + Generator appear in a "WSL" folder. This would likely require a more elaborate |
| 278 | + filtering syntax, to be able to select only profiles where a certain property |
| 279 | + has a specific value. Consider the user who has multiple "SSH |
| 280 | + me@\<some host\>.com" profiles, and they want all their "SSH\*" profiles to |
| 281 | + appear in an "SSH" folder. This feels out-of-scope for this spec. |
| 282 | +* A similar structure could potentially also be used for customizing the context |
| 283 | + menu within a control, or the context menu for the tab. (see [#3337]) |
| 284 | + - In both of those cases, it might be important to somehow refer to the |
| 285 | + context of the current tab or control in the json. Think for example about |
| 286 | + "Close tab" or "Close other tabs" - currently, those work by _knowing_ which |
| 287 | + tab the "action" is specified for, not by actually using a `closeTab` action. |
| 288 | + In the future, they might need to be implemented as something like |
| 289 | + - Close Tab: `{ "action": "closeTab", "index": "${selectedTab.index}" }` |
| 290 | + - Close Other Tabs: `{ "action": "closeTabs", "otherThan": "${selectedTab.index}" }` |
| 291 | + - Close Tabs to the Right: `{ "action": "closeTabs", "after": "${selectedTab.index}" }` |
| 292 | + |
| 293 | + |
| 294 | +<!-- Footnotes --> |
| 295 | +[#2046]: https://github.com/microsoft/terminal/issues/2046 |
| 296 | +[Command Palette, Addendum 1]: https://github.com/microsoft/terminal/blob/master/doc/specs/%232046%20-%20Unified%20keybindings%20and%20commands%2C%20and%20synthesized%20action%20names.md |
| 297 | + |
| 298 | +[#3337]: https://github.com/microsoft/terminal/issues/3337 |
| 299 | +[#6899]: https://github.com/microsoft/terminal/issues/6899 |
| 300 | +[#7175]: https://github.com/microsoft/terminal/issues/7175 |
0 commit comments