Merged PR 4838632: Merge OSS up to 58f5d7c7
Related work items: MSFT:27172323
3
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
@ -9,7 +9,8 @@
|
|||
* [ ] Closes #xxx
|
||||
* [ ] CLA signed. If not, go over [here](https://cla.opensource.microsoft.com/microsoft/Terminal) and sign the CLA
|
||||
* [ ] Tests added/passed
|
||||
* [ ] Requires documentation to be updated
|
||||
* [ ] Documentation updated. If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/terminal) and link it here: #xxx
|
||||
* [ ] Schema updated.
|
||||
* [ ] I've discussed this with core contributors already. If not checked, I'm ready to accept this work might be rejected in favor of a different grand plan. Issue number where discussion took place: #xxx
|
||||
|
||||
<!-- Provide a more detailed description of the PR, other things fixed or any additional comments/features here -->
|
||||
|
|
|
@ -7,16 +7,19 @@ EXPCMDFLAGS
|
|||
EXPCMDSTATE
|
||||
fullkbd
|
||||
href
|
||||
IAsync
|
||||
IBox
|
||||
IBind
|
||||
IClass
|
||||
IComparable
|
||||
ICustom
|
||||
IDirect
|
||||
IExplorer
|
||||
IMap
|
||||
IObject
|
||||
IStorage
|
||||
LCID
|
||||
LSHIFT
|
||||
NCHITTEST
|
||||
NCLBUTTONDBLCLK
|
||||
NCRBUTTONDBLCLK
|
||||
|
@ -24,7 +27,9 @@ NOAGGREGATION
|
|||
NOREDIRECTIONBITMAP
|
||||
oaidl
|
||||
ocidl
|
||||
RETURNCMD
|
||||
rfind
|
||||
roundf
|
||||
RSHIFT
|
||||
SIZENS
|
||||
tmp
|
||||
|
|
|
@ -28673,11 +28673,8 @@ attacco
|
|||
attach
|
||||
attachable
|
||||
attachableness
|
||||
attache
|
||||
attached
|
||||
attachedly
|
||||
attacher
|
||||
attachers
|
||||
attaches
|
||||
attacheship
|
||||
attaching
|
||||
|
@ -76036,6 +76033,7 @@ Clichy
|
|||
Clichy-la-Garenne
|
||||
click
|
||||
click-clack
|
||||
clickable
|
||||
clicked
|
||||
clicker
|
||||
clickers
|
||||
|
@ -186987,6 +186985,8 @@ hyperleucocytosis
|
|||
hyperleucocytotic
|
||||
hyperleukocytosis
|
||||
hyperlexis
|
||||
hyperlink
|
||||
hyperlinking
|
||||
hyperlipaemia
|
||||
hyperlipaemic
|
||||
hyperlipemia
|
||||
|
@ -190749,6 +190749,7 @@ Imer
|
|||
Imerina
|
||||
Imeritian
|
||||
IMF
|
||||
img
|
||||
IMHO
|
||||
imi
|
||||
imid
|
||||
|
@ -252147,6 +252148,7 @@ MrsSmith
|
|||
MRTS
|
||||
MRU
|
||||
Mru
|
||||
mru
|
||||
M.S.
|
||||
MS
|
||||
MS.
|
||||
|
@ -470900,6 +470902,7 @@ wild-born
|
|||
wild-brained
|
||||
wild-bred
|
||||
wildcard
|
||||
wildcards
|
||||
wildcat
|
||||
wildcats
|
||||
wildcatted
|
||||
|
|
|
@ -1746,6 +1746,7 @@ popup
|
|||
POPUPATTR
|
||||
PORFLG
|
||||
positionals
|
||||
posix
|
||||
POSTCHARBREAKS
|
||||
POSX
|
||||
POSXSCROLL
|
||||
|
@ -1976,6 +1977,7 @@ robomac
|
|||
roundtrip
|
||||
ROWSTOSCROLL
|
||||
RRF
|
||||
RRRGGGBB
|
||||
rtcore
|
||||
RTEXT
|
||||
rtf
|
||||
|
@ -2253,6 +2255,7 @@ targetnametoken
|
|||
targetver
|
||||
taskbar
|
||||
tbar
|
||||
TBase
|
||||
tbc
|
||||
tbi
|
||||
Tbl
|
||||
|
@ -2295,6 +2298,7 @@ testtestabc
|
|||
testtesttesttesttest
|
||||
TEXCOORD
|
||||
texel
|
||||
TExpected
|
||||
textattribute
|
||||
TEXTATTRIBUTEID
|
||||
Textbox
|
||||
|
@ -2319,6 +2323,7 @@ tilunittests
|
|||
Timeline
|
||||
titlebar
|
||||
TITLEISLINKNAME
|
||||
TJson
|
||||
tl
|
||||
TLEN
|
||||
Tlg
|
||||
|
@ -2341,6 +2346,7 @@ tooltip
|
|||
TOPDOWNDIB
|
||||
TOPLEFT
|
||||
TOPRIGHT
|
||||
TOpt
|
||||
tosign
|
||||
touchpad
|
||||
towlower
|
||||
|
@ -2383,6 +2389,7 @@ Txtev
|
|||
typechecked
|
||||
typechecking
|
||||
typedef
|
||||
typeid
|
||||
typeinfo
|
||||
typelib
|
||||
typename
|
||||
|
|
463
OpenConsole.sln
|
@ -62,6 +62,10 @@ If you have any issues when installing/upgrading the package please go to the [W
|
|||
|
||||
---
|
||||
|
||||
## Windows Terminal 2.0 Roadmap
|
||||
|
||||
The plan for delivering Windows Terminal 2.0 [is described here](/doc/terminal-v2-roadmap.md) and will be updated as the project proceeds.
|
||||
|
||||
## Project Build Status
|
||||
|
||||
Project|Build Status
|
||||
|
|
|
@ -47,7 +47,6 @@ stages:
|
|||
- stage: Build_x86
|
||||
displayName: Build x86
|
||||
dependsOn: []
|
||||
condition: not(eq(variables['Build.Reason'], 'PullRequest'))
|
||||
jobs:
|
||||
- template: ./templates/build-console-ci.yml
|
||||
parameters:
|
||||
|
|
38
build/rules/CollectWildcardResources.targets
Normal file
|
@ -0,0 +1,38 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="16.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="BeforeGenerateProjectPriFile" DependsOnTargets="OpenConsoleCollectWildcardPRIFiles" />
|
||||
|
||||
<!--
|
||||
The vcxproj system does not support wildcards at the root level of a project.
|
||||
This poses a problem, as we want to include resw files that are not checked into the
|
||||
repository. Since they're usually localized and stored in directories named after
|
||||
their languages, we can't exactly explicitly simultaneously list them all and remain
|
||||
sane. We want to use wildcards to make our lives easier.
|
||||
|
||||
This rule takes OCResourceDirectory items and includes all resw files that live
|
||||
underneath them.
|
||||
|
||||
** TIRED **
|
||||
(does not work because of wildcards)
|
||||
<PRIResource Include="Resources/*/Resources.resw" />
|
||||
|
||||
** WIRED **
|
||||
(keep the en-US resource in the project, because it is checked in and VS will show it)
|
||||
<PRIResource Include="Resources/en-US/Resources.resw" />
|
||||
<OCResourceDirectory Include="Resources" />
|
||||
-->
|
||||
<Target Name="OpenConsoleCollectWildcardPRIFiles">
|
||||
<CreateItem Include="@(OCResourceDirectory->'%(Identity)\**\*.resw')">
|
||||
<Output TaskParameter="Include" ItemName="_OCFoundPRIFiles" />
|
||||
</CreateItem>
|
||||
<ItemGroup>
|
||||
<_OCFoundPRIFiles Include="@(PRIResource)" />
|
||||
<PRIResource Remove="@(PRIResource)" />
|
||||
<PRIResource Include="@(_OCFoundPRIFiles->Distinct())" />
|
||||
</ItemGroup>
|
||||
<Message Text="$(ProjectName) (wildcard PRIs) -> @(PRIResource)" />
|
||||
</Target>
|
||||
</Project>
|
|
@ -231,6 +231,32 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"OpenSettingsAction": {
|
||||
"description": "Arguments corresponding to a Open Settings Action",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ShortcutAction"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "string",
|
||||
"pattern": "openSettings"
|
||||
},
|
||||
"target": {
|
||||
"type": "string",
|
||||
"default": "settingsFile",
|
||||
"description": "The settings file to open.",
|
||||
"enum": [
|
||||
"settingsFile",
|
||||
"defaultsFile",
|
||||
"allFiles"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"Keybinding": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
|
@ -245,6 +271,7 @@
|
|||
{ "$ref": "#/definitions/MoveFocusAction" },
|
||||
{ "$ref": "#/definitions/ResizePaneAction" },
|
||||
{ "$ref": "#/definitions/SplitPaneAction" },
|
||||
{ "$ref": "#/definitions/OpenSettingsAction" },
|
||||
{ "type": "null" }
|
||||
]
|
||||
},
|
||||
|
|
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 15 KiB |
BIN
doc/specs/#1502 - Advanced Tab Switcher/img/VSMinimumSize.png
Normal file
After Width: | Height: | Size: 92 KiB |
After Width: | Height: | Size: 345 KiB |
BIN
doc/specs/#1502 - Advanced Tab Switcher/img/VSTabSwitcher.png
Normal file
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 34 KiB |
227
doc/specs/#1502 - Advanced Tab Switcher/spec.md
Normal file
|
@ -0,0 +1,227 @@
|
|||
---
|
||||
author: Leon Liang @leonMSFT
|
||||
created on: 2019-11-27
|
||||
last updated: 2020-06-16
|
||||
issue id: 1502
|
||||
---
|
||||
|
||||
# Advanced Tab Switcher
|
||||
|
||||
## Abstract
|
||||
|
||||
Currently the user is able to cycle through tabs on the tab bar. However, this horizontal cycling can be pretty inconvenient when the tab titles are long or when there are too many tabs on the tab bar. It could also get hard to see all your available tabs if the tab titles are long and your screen is small. In addition, there's a common use case to quickly switch between two tabs, e.g. when one tab is used as reference and the other is the actively worked-on tab. If the tabs are not right next to each other on the tab bar, it could be difficult to quickly swap between the two. Having the tabs displayed in Most Recently Used (MRU) order would help with this problem. It could also make the user experience better when there are a handful of tabs that are frequently used, but are nowhere near each other on the tab bar.
|
||||
|
||||
Having a tab switcher UI, like the ones in Visual Studio and Visual Studio Code, could help with the tab experience. Presenting the tabs vertically in their own little UI allows the user to see more of the tabs at once, compared to scanning the tab row horizontally and scrolling left/right to find the tab you want. The tab order in those tab switchers are also in MRU order by default.
|
||||
|
||||
To try to alleviate some of these user scenarios, we want to create a tab switcher similar to the ones found in VSCode and VS. This spec will cover the design of the switcher, and how a user would interact with the switcher. It would be primarily keyboard driven, and would give a pop-up display of a vertical list of tabs. The tab switcher would also be able to display the tabs in Most Recently Used (MRU) order.
|
||||
|
||||
## Inspiration
|
||||
|
||||
This was mainly inspired by the tab switcher that's found in Visual Studio Code and Visual Studio.
|
||||
|
||||
VS Code's tab switcher appears directly underneath the tab bar.
|
||||
|
||||
![Visual Studio Code Tab Switcher](img/VSCodeTabSwitcher.png)
|
||||
|
||||
Visual Studio's tab switcher presents itself as a box in the middle of the editor.
|
||||
|
||||
![Visual Studio Tab Switcher](img/VSTabSwitcher.png)
|
||||
|
||||
In terms of navigating the switcher, both VSCode and Visual Studio behave very similarly. Both open with the press of <kbd>ctrl+tab</kbd> and dismiss on release of <kbd>ctrl</kbd>. They both also allow the user to select the tab with the mouse and with <kbd>enter</kbd>. <kbd>esc</kbd> and a mouse click outside of the switcher both dismiss the window as well.
|
||||
|
||||
I'm partial towards looking like VSCode's Tab Switcher - specifically because it seems like both their Command Palette and Tab Switcher use the same UI. You can observe this by first bringing up the command palette, then hitting the keybinding to bring up the tab switcher. You'll notice that they're both using the same centered drop-down from the tab row. In fact, hitting the Tab Switcher keybinding in VSCode while the Command Palette is open simply auto fills the search box with "edit active", signifying that the user wants to select one of the tabs to edit, effectively "swapping" to the tab that's highlighted.
|
||||
|
||||
Since Terminal now has a command palette, it would be amazing to reuse that UI and simply fill it with the names of a user's currently open tabs!
|
||||
|
||||
## Solution Design
|
||||
|
||||
To extend upon the command palette, we simply need to create and maintain two Vector<Commands>, where each command will simply dispatch a `SwitchToTab` `ShortcutAction`. One vector will have the commands in tab row order, and the other will be in MRU order. They'll both have to be maintained along with our existing vector of tabs.
|
||||
|
||||
These vectors of commands can then be set as the commands to pull from in the command palette, and as long as the tab titles are available in these commands, the command palette will be able to naturally filter through the tabs as a user types in its search bar. Just like the command palette, a user will be able to navigate through the list of tabs with the arrow keys and pointer interactions. As part of this implementation, I can supplement these actions with "tab switcher specific" navigation keybindings that would only work if the command palette is in tab switcher mode.
|
||||
|
||||
The `TabSwitcherControl` will use `TerminalPage`'s `ShortcutActionDispatch` to dispatch a `SwitchToTab` `ShortcutAction`. This will eventually cause `TerminalPage::_OnTabSelectionChanged` to be called. We can update the MRU in this function to be sure that changing tabs from the TabSwitcher, clicking on a tab, or nextTab/prevTab-ing will keep the MRU up-to-date. Adding or closing tabs are handled in `_OpenNewTab` and `_CloseFocusedTab`, which will need to be modified to update the command vectors.
|
||||
|
||||
## UI/UX Design
|
||||
|
||||
The Tab Switcher will reuse a lot of the XAML code that's used in the command palette. This means it'll show up as a drop-down from the horizontal center of the tab row. It'll appear as a single overlay over the whole Terminal window. There will also be a search box on top of the list of tabs. Here's a rough mockup of how the command palette/tab switcher looks like:
|
||||
|
||||
![Mockup Command Palette with Tab Titles](img/CommandPaletteExample.png)
|
||||
|
||||
Each entry in the list will show the tab's titles and their assigned number for quick switching, and only one line will be highlighted to signify the tab that is currently selected. The top 9 tabs in the list are numbered for quick switching, and the rest of the tabs will simply have an empty space where a number would be.
|
||||
|
||||
The list would look (roughly) like this:
|
||||
```
|
||||
1 foo (highlighted)
|
||||
2 boo
|
||||
3 Windows
|
||||
4 /c/Users/booboo
|
||||
5 Git Moo
|
||||
6 shoo
|
||||
7 /c/
|
||||
8 /d/
|
||||
9 /e/
|
||||
/f/
|
||||
/g/
|
||||
/h/
|
||||
```
|
||||
|
||||
The highlighted line can move up or down, and if the user moves up while the highlighted line is already at the top of the list, the highlight will wrap around to the bottom of the list. Similarly, it will wrap to the top if the highlight is at the bottom of the list and the user moves down.
|
||||
|
||||
If there's more tabs than the UI can display, the list of tabs will scroll up/down as the user keeps iterating up/down. Even if some of the numbered tabs (the first 9 tabs) are not visible, the user can still press any number 1 through 9 to quick switch to that tab.
|
||||
|
||||
To give an example of what happens after scrolling past the end, imagine a user is starting from the state in the mock above. The user then iterates down past the end of the visible list four times. The below mock shows the result.
|
||||
|
||||
```
|
||||
5 Git Moo
|
||||
6 shoo
|
||||
7 /c/
|
||||
8 /d
|
||||
9 /e/
|
||||
/f/
|
||||
/g/
|
||||
/h/
|
||||
/i/
|
||||
/j/
|
||||
/k/
|
||||
/l/ (highlighted)
|
||||
```
|
||||
|
||||
The tabs designated by numbers 1 through 4 are no longer visible (but still quick-switchable), and the list now starts with "Git Moo", which is associated with number 5.
|
||||
|
||||
### Using the Switcher
|
||||
|
||||
#### Opening the Tab Switcher
|
||||
|
||||
The user can press a keybinding named `tabSwitcher` to bring up the command palette UI with a list of tab titles.
|
||||
The user can also bring up the command palette first, and type a "tab switcher" prefix like "@" into the search bar to switch into "tab switcher mode".
|
||||
The user will be able to change it to whatever they like.
|
||||
There will also be an optional `anchor` arg that may be provided to this keybinding.
|
||||
|
||||
#### Keeping it open
|
||||
|
||||
We use the term `anchor` to illustrate the idea that the UI stays visible as long as something is "anchoring" it down.
|
||||
|
||||
Here's an example of how to set the `anchor` key in the settings:
|
||||
```
|
||||
{"keys": ["ctrl+tab"], "command": {"action": "openTabSwitcher", "anchor": "ctrl" }}
|
||||
```
|
||||
|
||||
This user provided the `anchor` key arg, and set it to <kbd>ctrl</kbd>. So, the user would open the UI with <kbd>ctrl+tab</kbd>, and as long as the user is holding <kbd>ctrl</kbd> down, the UI won't dismiss. The moment the user releases <kbd>ctrl</kbd>, the UI dismisses. The `anchor` key needs to be one of the keys in the `openTabSwitcher` keybinding. If it isn't, we'll display a warning dialog in this case saying that the `anchor` key isn't actually part of the keybinding, and the user might run into some weird behavior.
|
||||
|
||||
If `openTabSwitcher` is not given an `anchor` key, the switcher will stay visible even after the release of the keybinding.
|
||||
|
||||
#### Switching through Tabs
|
||||
|
||||
The user will be able to navigate through the switcher with the following keybindings:
|
||||
|
||||
- Switching Down: <kbd>tab</kbd> or <kbd>downArrow</kbd>
|
||||
- Switching Up: <kbd>shift+tab</kbd> or <kbd>upArrow</kbd>
|
||||
|
||||
As the user is cycling through the tab list, the selected tab will be highlighted but the terminal won't actually switch focus to the selected tab. This also applies to pointer interaction. Hovering over an item with a mouse will highlight the item but not switch to the tab.
|
||||
|
||||
#### Closing the Switcher and Bringing a Tab into Focus
|
||||
|
||||
There are two _dismissal_ keybindings:
|
||||
|
||||
1. <kbd>enter</kbd> : brings the currently selected tab into focus and dismisses the UI.
|
||||
2. <kbd>esc</kbd> : dismisses the UI without changing tab focus.
|
||||
|
||||
The following are ways a user can dismiss the UI, _whether or not_ the `Anchor` key is provided to `openTabSwitcher`.
|
||||
|
||||
1. The user can press a number associated with a tab to instantly switch to the tab and dismiss the switcher.
|
||||
2. The user can click on a tab to instantly switch to the tab and dismiss the switcher.
|
||||
3. The user can click outside of the UI to dismiss the switcher without bringing the selected tab into focus.
|
||||
4. The user can press any of the dismissal keybindings.
|
||||
|
||||
If the `anchor` key is provided, then in addition to the above methods, the UI will dismiss upon the release of the `anchor` key.
|
||||
|
||||
Pressing the `openTabSwitcher` keychord again will not close the switcher, it'll do nothing.
|
||||
|
||||
### Most Recently Used Order
|
||||
|
||||
We'll provide a setting that will allow the list of tabs to be presented in either _in-order_ (how the tabs are ordered on the tab bar), or _Most Recently Used Order_ (MRU). MRU means that the tab that the terminal most recently visited will be on the top of the list, and the tab that the terminal has not visited for the longest time will be on the bottom.
|
||||
|
||||
There will be an argument for the `openTabSwitcher` action called `displayOrder`. This can be either `inOrder` or `mruOrder`. Making the setting an argument passed into `openTabSwitcher` would allow the user to have one keybinding to open an MRU Tab Switcher, and different one for the In-Order Tab Switcher. For example:
|
||||
```
|
||||
{"keys": ["ctrl+tab"], "command": {"action": "openTabSwitcher", "anchor":"ctrl", "displayOrder":"mruOrder"}}
|
||||
{"keys": ["ctrl+shift+p"], "command": {"action": "openTabSwitcher", "anchor":"ctrl", "displayOrder":"inOrder"}}
|
||||
```
|
||||
By default (when the arg isn't specified), `displayOrder` will be "mruOrder".
|
||||
|
||||
### Numbered Tabs
|
||||
|
||||
Similar to how the user can currently switch to a particular tab with a combination of keys such as <kbd>ctrl+shift+1</kbd>, we want to have the tab switcher provide a number to the first nine tabs (1-9) in the list for quick switching. If there are more than nine tabs in the list, then the rest of the tabs will not have a number assigned.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### Accessibility
|
||||
|
||||
- The tab switcher will be using WinUI, and so it'll be automatically linked to the UIA tree. This allows screen readers to find it, and so narrator will be able to navigate the switcher easily.
|
||||
- The UI is also fully keyboard-driven, with the option of using a mouse to interact with the UI.
|
||||
- When the tab switcher pops up, the focus immediately swaps to it.
|
||||
- For the sake of more contrast with the background, we could use a ThemeShadow to bring the UI closer to the user, making the focus clearer.
|
||||
|
||||
### Security
|
||||
|
||||
This shouldn't introduce any security issues.
|
||||
|
||||
### Reliability
|
||||
|
||||
How we're updating the MRU is something to watch out for since it triggers on a lot of tab interactions. However, I don't foresee the update taking long at all, and I can't imagine that users can create and delete tabs fast enough to matter.
|
||||
|
||||
### Compatibility
|
||||
|
||||
- The existing way of navigating horizontally through the tabs on the tab bar should not break.
|
||||
- These should also be separate keybindings from the keybindings associated with using the tab switcher.
|
||||
- When a user reorders their tabs on the tab bar, the MRU order remains unchanged. For example:
|
||||
- Tab Bar:`[cmd(focused), ps, wsl]` and MRU:`[cmd, ps, wsl]`
|
||||
- Reordered Tab Bar:`[wsl, cmd(focused), ps]` and MRU:`[cmd, ps, wsl]`
|
||||
|
||||
### Performance, Power, and Efficiency
|
||||
|
||||
## Potential Issues
|
||||
|
||||
We'll need to be careful about how the UI is presented depending on different sizes of the terminal. We also should test how the UI looks as it's open and resizing is happening. Visual Studio's tab switcher is a fixed size, and is always in the middle. Even when the VS window is smaller than the tab switcher size, the tab switcher will show up larger than the VS window itself.
|
||||
|
||||
![Small Visual Studio Without Tab Switcher](img/VSMinimumSize.png)
|
||||
![Small Visual Studio With Tab Switcher](img/VSMinimumSizeWithTabSwitcher.png)
|
||||
|
||||
Visual Studio Code only allows the user to shrink the window until it hits a minimum width and height. This minimum width and height gives its tab switcher enough space to show a meaningful amount of information.
|
||||
|
||||
![Small Visual Studio Code with Tab Switcher](img/VSCodeMinimumTabSwitcherSize.png)
|
||||
|
||||
Terminal can't really replicate Visual Studio's version of the tab switcher in this situation. The TabSwitcher needs to be contained within the Terminal. So, if the TabSwitcher is always centered and has a percentage padding from the borders of the Terminal, it'll shrink as Terminal shrinks. Since the Terminal also has a minimum width, the switcher should always have enough space to be usefully visible.
|
||||
|
||||
## Future considerations
|
||||
|
||||
### Pane Navigation
|
||||
|
||||
There was discussion in [#1502] that brought up the idea of pane navigation, inspired by tmux.
|
||||
|
||||
![Tmux Tab and Pane Switching](img/tmuxPaneSwitching.png)
|
||||
|
||||
Tmux allows the user to navigate directly to a pane and even give a preview of the pane. This would be extremely useful since it would allow the user to see a tree of their open tabs and panes. Currently there's no way to see what panes are open in each tab, so if you're looking for a particular pane, you'd need to cycle through your tabs to find it. If something like pane profile names (not sure what information to present in the switcher for panes) were presented in the TabSwitcher, the user could see all the panes in one box.
|
||||
|
||||
To support pane navigation, the tab switcher can simply have another column to the right of the tab list to show a list of panes inside the selected tab. As the user iterates through the tab list, they can simply hit right to dig deeper into the tab's panes, and hit left to come back to the tab list. Each tab's list of panes will be MRU or in-order, depending on which `displayOrder` arg was provided to the `openTabSwitcher` keybinding.
|
||||
|
||||
Pane navigation is a clear next step to build on top of the tab switcher, but this spec will specifically deal with just tab navigation in order to keep the scope tight. The tab switcher implementation just needs to allow for pane navigation to be added in later.
|
||||
|
||||
### Tab Preview on Hover
|
||||
|
||||
With this feature, having a tab highlighted in the switcher would make the Terminal display that tab as if it switched to it. I believe currently there is no way to set focus to a tab in a "preview" mode. This is important because MRU updates whenever a tab is focused, but we don't want the MRU to update on a preview. Given that this feature is a "nice thing to have", I'll leave it for
|
||||
after the tab switcher has landed.
|
||||
|
||||
## Resources
|
||||
|
||||
Feature Request: An advanced tab switcher [#1502]
|
||||
Ctrl+Tab toggle between last two windows like Alt+Tab [#973]
|
||||
The Command Palette Thread [#2046]
|
||||
The Command Palette Spec [#5674]
|
||||
Feature Request: Search [#605]
|
||||
|
||||
<!-- Footnotes -->
|
||||
[#605]: https://github.com/microsoft/terminal/issues/605
|
||||
[#973]: https://github.com/microsoft/terminal/issues/973
|
||||
[#1502]: https://github.com/microsoft/terminal/issues/1502
|
||||
[#2046]: https://github.com/microsoft/terminal/issues/2046
|
||||
[#5674]: https://github.com/microsoft/terminal/pull/5674
|
795
doc/specs/#2046 - Command Palette.md
Normal file
|
@ -0,0 +1,795 @@
|
|||
---
|
||||
author: Mike Griese @zadjii-msft
|
||||
created on: 2019-08-01
|
||||
last updated: 2020-06-16
|
||||
issue id: 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](https://github.com/microsoft/terminal/issues/2046#issuecomment-514219791)
|
||||
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](https://github.com/microsoft/terminal/issues/4632) 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:
|
||||
|
||||
```js
|
||||
{
|
||||
"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`:
|
||||
|
||||
```c++
|
||||
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:
|
||||
|
||||
```json
|
||||
"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 the `name` and `icon`
|
||||
properties of a `Profile` 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](#localization), we'd update the built-in commands to the following:
|
||||
|
||||
```json
|
||||
"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:
|
||||
|
||||
```json
|
||||
"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 **P**ane", "[-] S**p**lit Horizontal", "[ | ]
|
||||
S**p**lit Vertical", "**P**rev Tab", "O**p**en Settings" and "O**p**en 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 <kbd>enter</kbd>, 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](#Configuring-The-ActionCommandline-Mode-Prefix)".
|
||||
|
||||
### 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
|
||||
```json
|
||||
{ "icon": null, "name": { "key": "NewTabCommandName" }, "action": "newTab" },
|
||||
```
|
||||
|
||||
Could be overridden with the command:
|
||||
```json
|
||||
{ "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`:
|
||||
|
||||
```json
|
||||
{ "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 actual
|
||||
`ShortcutAction`.
|
||||
|
||||
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:
|
||||
|
||||
```json
|
||||
"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.
|
||||
|
||||
```json
|
||||
"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, and `suppressNewline`, an optional bool, defaulted to false. The
|
||||
`inputCommand` action would deliver the given `commandline` 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 like `vim`, 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 "**O**pen **T**ab" than
|
||||
"**O**pen Se**t**tings").
|
||||
* 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.
|
||||
* 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:
|
||||
```json
|
||||
"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
|
||||
* 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's `commandline`
|
||||
* 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.
|
||||
|
||||
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 <kbd>enter</kbd>'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.
|
||||
|
||||
```json
|
||||
{
|
||||
"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](https://github.com/microsoft/terminal/issues/2046)
|
||||
|
||||
Keybindings args: #[1349](https://github.com/microsoft/terminal/pull/1349)
|
||||
|
||||
Cascading User & Default Settings: #[754](https://github.com/microsoft/terminal/issues/754)
|
||||
|
||||
Untie "active control" from "currently XAML-focused control" #[1205](https://github.com/microsoft/terminal/issues/1205)
|
||||
|
||||
Allow dropdown menu customization in profiles.json [#1571](https://github.com/microsoft/terminal/issues/1571)
|
||||
|
||||
Search or run a command in Dropdown menu [#3879]
|
||||
|
||||
Spec: Introduce a mini-specification for localized resource use from JSON [#5280]
|
||||
|
||||
<!-- Footnotes -->
|
||||
[#754]: https://github.com/microsoft/terminal/issues/754
|
||||
[#1205]: https://github.com/microsoft/terminal/issues/1205
|
||||
[#1142]: https://github.com/microsoft/terminal/pull/1349
|
||||
[#2046]: https://github.com/microsoft/terminal/issues/2046
|
||||
[#1571]: https://github.com/microsoft/terminal/issues/1571
|
||||
[#3879]: https://github.com/microsoft/terminal/issues/3879
|
||||
[#5280]: https://github.com/microsoft/terminal/pull/5280
|
||||
[#4472]: https://github.com/microsoft/terminal/issues/4472
|
||||
[#3327]: https://github.com/microsoft/terminal/issues/3327
|
||||
[#3753]: https://github.com/microsoft/terminal/pulls/3753
|
||||
[#5888]: https://github.com/microsoft/terminal/pulls/5888
|
|
@ -0,0 +1,608 @@
|
|||
---
|
||||
author: Mike Griese @zadjii-msft
|
||||
created on: 2020-06-15
|
||||
last updated: 2020-06-19
|
||||
issue id: 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:
|
||||
|
||||
```json
|
||||
{
|
||||
"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.
|
||||
1. Unify keybindings and commands, so both `keybindings` and `commands` can
|
||||
specify either actions bound to keys, and/or actions bound to entries in the
|
||||
Command Palette.
|
||||
2. 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 in `command`.
|
||||
* Anything with a `name`<sup>[1]</sup> 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 a `keys` 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 valid
|
||||
`iterateOn` 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.
|
||||
|
||||
<sup>[1]</sup>: 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:
|
||||
|
||||
```json
|
||||
"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:
|
||||
* <kbd>ctrl+alt+a</kbd>: This key chord is bound to the "Duplicate tab"
|
||||
(`duplicateTab`) action, which is also bound to the command with the same
|
||||
name.
|
||||
* <kbd>ctrl+alt+b</kbd>: This key chord is bound to the `nextTab` action, which
|
||||
doesn't have an associated command.
|
||||
* <kbd>ctrl+alt+c</kbd>: 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.
|
||||
* <kbd>ctrl+alt+d</kbd>: This is the same as the above, but with the "Connect to
|
||||
ssh../second.com" action.
|
||||
* <kbd>ctrl+alt+e</kbd>: 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...
|
||||
```
|
||||
* <kbd>ctrl+alt+f</kbd>: 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 a `GenerateName()` method defined. This will create a
|
||||
string describing the `ShortcutAction` and it's associated `ActionArgs`.
|
||||
- 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.
|
||||
* When we're parsing commands, if a command doesn't have a `name`, we'll instead
|
||||
attempt to use `GenerateName` to create the unique string for the action
|
||||
associated with this command. If the command does have a `name` 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.
|
||||
|
||||
[**Appendix 1**](#appendix-1-name-generation-samples-for-ShortcutActions) 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.
|
||||
* 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
|
||||
```json
|
||||
{ "command": { "action": "copy", "singleLine": true } }
|
||||
```
|
||||
to `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:
|
||||
|
||||
```json
|
||||
"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**](#appendix-2-complete-default-command-palette).
|
||||
|
||||
## 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_
|
||||
* `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
|
||||
```
|
||||
|
||||
|
||||
<!-- Footnotes -->
|
||||
[Command Palette Spec]: https://github.com/microsoft/terminal/blob/master/doc/specs/%232046%20-%20Command%20Palette.md
|
135
doc/specs/#2557 - Settings Keybinding.md
Normal file
|
@ -0,0 +1,135 @@
|
|||
---
|
||||
author: Carlos Zamora @carlos-zamora
|
||||
created on: 2020-05-14
|
||||
last updated: 2020-05-14
|
||||
issue id: #2557
|
||||
---
|
||||
|
||||
# Open Settings Keybinding
|
||||
|
||||
## Abstract
|
||||
|
||||
This spec outlines an expansion to the existing `openSettings` keybinding.
|
||||
|
||||
## Inspiration
|
||||
|
||||
As a Settings UI becomes more of a reality, the behavior of this keybinding will be expanded on to better interact with the UI. Prior to a Settings UI, there was only one concept of the modifiable user settings: settings.json.
|
||||
|
||||
Once the Settings UI is created, we can expect users to want to access the following scenarios:
|
||||
- Settings UI: globals page
|
||||
- Settings UI: profiles page
|
||||
- Settings UI: color schemes page
|
||||
- Settings UI: keybindings page
|
||||
- settings.json
|
||||
- defaults.json
|
||||
These are provided as non-comprehensive examples of pages that might be in a future Settings UI. The rest of the doc assumes these are the pages in the Settings UI.
|
||||
|
||||
|
||||
## Solution Design
|
||||
Originally, #2557 was intended to allow for a keybinding arg to access defaults.json. I imagined a keybinding arg such as "openDefaults: true/false" to accomplish this. However, this is not expandable in the following scenarios:
|
||||
- what if we decide to create more settings files in the future? (i.e. themes.json, extensions.json, etc...)
|
||||
- when the Settings UI comes in, there is ambiguity as to what `openSettings` does (json? UI? Which page?)
|
||||
|
||||
### Proposition 1.1: the minimal `target` arg
|
||||
Instead, what if we introduced a new `target` keybinding argument, that could be used as follows:
|
||||
| Keybinding Command | Behavior |
|
||||
|--|--|
|
||||
| `"command": { "action": "openSettings", "target": "settingsFile" }` | opens "settings.json" in your default text editor |
|
||||
| `"command": { "action": "openSettings", "target": "defaultsFile" }` | opens "defaults.json" in your default text editor |
|
||||
| `"command": { "action": "openSettings", "target": "allSettingsFiles" }` | opens all of settings files in your default text editor |
|
||||
| `"command": { "action": "openSettings", "target": "settingsUI" }` | opens the Settings UI |
|
||||
|
||||
This was based on Proposition 1 below, but reduced the overhead of people able to define specific pages to go to.
|
||||
|
||||
### Other options we considered were...
|
||||
|
||||
#### Proposition 1: the `target` arg
|
||||
We considered making target be more specific like this:
|
||||
| Keybinding Command | Behavior |
|
||||
|--|--|
|
||||
| `"command": { "action": "openSettings", "target": "settingsFile" }` | opens "settings.json" in your default text editor |
|
||||
| `"command": { "action": "openSettings", "target": "defaultsFile" }` | opens "defaults.json" in your default text editor |
|
||||
| `"command": { "action": "openSettings", "target": "uiSettings" }` | opens the Settings UI |
|
||||
| `"command": { "action": "openSettings", "target": "uiGlobals" }` | opens the Settings UI to the Globals page |
|
||||
| `"command": { "action": "openSettings", "target": "uiProfiles" }` | opens the Settings UI to the Profiles page |
|
||||
| `"command": { "action": "openSettings", "target": "uiColorSchemes" }` | opens the Settings UI to the Color Schemes page |
|
||||
|
||||
If the Settings UI does not have a home page, `uiGlobals` and `uiSettings` will do the same thing.
|
||||
|
||||
This provides the user with more flexibility to decide what settings page to open and how to access it.
|
||||
|
||||
#### Proposition 2: the `format` and `page` args
|
||||
Another approach would be to break up `target` into `format` and `page`.
|
||||
|
||||
`format` would be either `json` or `ui`, dictating how you can access the setting.
|
||||
`page` would be any of the categories we have for settings: `settings`, `defaults`, `globals`, `profiles`, etc...
|
||||
|
||||
This could look like this:
|
||||
| Keybinding Command | Behavior |
|
||||
|--|--|
|
||||
| `"command": { "action": "openSettings", "format": "json", "page": "settings" }` | opens "settings.json" in your default text editor |
|
||||
| `"command": { "action": "openSettings", "format": "json", "page": "defaults" }` | opens "defaults.json" in your default text editor |
|
||||
| `"command": { "action": "openSettings", "format": "ui", "page": "settings" }` | opens the Settings UI |
|
||||
| `"command": { "action": "openSettings", "format": "ui", "page": "globals" }` | opens the Settings UI to the Globals page |
|
||||
| `"command": { "action": "openSettings", "format": "ui", "page": "profiles" }` | opens the Settings UI to the Profiles page |
|
||||
| `"command": { "action": "openSettings", "format": "ui", "page": "colorSchemes" }` | opens the Settings UI to the Color Schemes page |
|
||||
|
||||
The tricky thing for this approach is, what do we do in the following scenario:
|
||||
```js
|
||||
{ "command": { "action": "openSettings", "format": "json", "page": "colorSchemes" } }
|
||||
```
|
||||
In situations like this, where the user wants a `json` format, but chooses a `page` that is a part of a larger settings file, I propose we simply open `settings.json` (or whichever file contains the settings for the desired feature).
|
||||
|
||||
#### Proposition 3: minimal approach
|
||||
What if we don't need to care about the page, and we really just cared about the format: UI vs json? Then, we still need a way to represent opening defaults.json. We could simplify Proposition 2 to be as follows:
|
||||
- `format`: `json`, `ui`
|
||||
- ~`page`~ `openDefaults`: `true`, `false`
|
||||
|
||||
Here, we take away the ability to specifically choose which page the user wants to open, but the result looks much cleaner.
|
||||
|
||||
If there are concerns about adding more settings files in the future, `openDefaults` could be renamed to be `target`, and this would still serve as a hybrid of Proposition 1 and 2, with less possible options.
|
||||
|
||||
## UI/UX Design
|
||||
|
||||
The user has full control over modifying and adding these keybindings.
|
||||
|
||||
However, the question arises for what the default experience should be. I propose the following:
|
||||
| Keychord | Behavior |
|
||||
| <kbd>ctrl+,</kbd> | Open settings.json |
|
||||
| <kbd>ctrl+alt+,</kbd> | Open defaults.json |
|
||||
|
||||
When the Settings UI gets added in, they will be updated to open their respective pages in the Settings UI.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### Accessibility
|
||||
|
||||
None.
|
||||
|
||||
### Security
|
||||
|
||||
None.
|
||||
|
||||
### Reliability
|
||||
|
||||
None.
|
||||
|
||||
### Compatibility
|
||||
|
||||
Users that expect a json file to open would have to update their keybinding to do so.
|
||||
|
||||
### Performance, Power, and Efficiency
|
||||
|
||||
## Potential Issues
|
||||
|
||||
None.
|
||||
|
||||
## Future considerations
|
||||
|
||||
When the Settings UI becomes available, a new value for `target` of `settingsUI` will be added and it will become the default target.
|
||||
|
||||
If the community finds value in opening to a specific page of the Settings UI, `target` will be responsible for providing that functionality.
|
||||
|
||||
## Resources
|
||||
|
||||
None.
|
|
@ -1,427 +0,0 @@
|
|||
---
|
||||
author: Mike Griese @zadjii-msft
|
||||
created on: 2019-08-01
|
||||
last updated: 2020-04-13
|
||||
issue id: 2046
|
||||
---
|
||||
|
||||
_This is a draft spec. It should be considered a work-in-progress._
|
||||
|
||||
# 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](https://github.com/microsoft/terminal/issues/2046#issuecomment-514219791)
|
||||
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.
|
||||
|
||||
## Solution Design
|
||||
|
||||
First off, for the sake of clarity, we'll rename the `command` of a keybinding
|
||||
to `action`. This will help keep the mental model between commands and actions
|
||||
clearer. When deserializing keybindings, we'll include a check for the old
|
||||
`command` key to migrate it.
|
||||
|
||||
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:
|
||||
|
||||
```js
|
||||
{
|
||||
"name": string,
|
||||
"action": string,
|
||||
"icon": string
|
||||
"args": object?,
|
||||
}
|
||||
```
|
||||
|
||||
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`:
|
||||
|
||||
```c++
|
||||
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
|
||||
`PerformAction(ActionAndArgs)` method (and events for each action).
|
||||
`AppKeyBindings` will be initialized with a reference to the
|
||||
`ShortcutActionDispatch` object, so that it can call `PerformAction` 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 `App`, when someone clicks on an item in the list, we'll get the
|
||||
`ActionAndArgs` associated with that list element, and call PerformAction 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:
|
||||
|
||||
```json
|
||||
"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 the `name` and `icon`
|
||||
properties of a `Profile` 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](#localization), we'd update the built-in commands to the following:
|
||||
|
||||
```json
|
||||
"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.
|
||||
|
||||
## 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.
|
||||
|
||||
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 attache 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 searching for
|
||||
commands, and a list of commands.
|
||||
|
||||
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:
|
||||
|
||||
```json
|
||||
"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": "splitHorizontal" },
|
||||
{ "icon": null, "name": "[ | ] Split Vertical", "action": "splitVertical" },
|
||||
{ "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 **P**ane", "[-] S**p**lit Horizontal", "[ | ]
|
||||
S**p**lit Vertical", "**P**rev Tab", "O**p**en Settings" and "O**p**en 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.
|
||||
|
||||
## 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 actual
|
||||
`ShortcutAction`.
|
||||
|
||||
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:
|
||||
|
||||
```json
|
||||
"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": "splitHorizontal" },
|
||||
{ "icon": null, "name": { "key": "SplitVerticalCommandKey" }, "action": "splitVertical" },
|
||||
{ "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].
|
||||
|
||||
|
||||
## 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, and `suppressNewline`, an optional bool, defaulted to false. The
|
||||
`inputCommand` action would deliver the given `commandline` 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 like `vim`, 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 "**O**pen **T**ab" than
|
||||
"**O**pen Se**t**tings").
|
||||
* 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.
|
||||
* 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.
|
||||
|
||||
## Resources
|
||||
Initial post that inspired this spec: #[2046](https://github.com/microsoft/terminal/issues/2046)
|
||||
|
||||
Keybindings args: #[1349](https://github.com/microsoft/terminal/pull/1349)
|
||||
|
||||
Cascading User & Default Settings: #[754](https://github.com/microsoft/terminal/issues/754)
|
||||
|
||||
Untie "active control" from "currently XAML-focused control" #[1205](https://github.com/microsoft/terminal/issues/1205)
|
||||
|
||||
Allow dropdown menu customization in profiles.json [#1571](https://github.com/microsoft/terminal/issues/1571)
|
||||
|
||||
Search or run a command in Dropdown menu [#3879]
|
||||
|
||||
Spec: Introduce a mini-specification for localized resource use from JSON [#5280]
|
||||
|
||||
<!-- Footnotes -->
|
||||
[#754]: https://github.com/microsoft/terminal/issues/754
|
||||
[#1205]: https://github.com/microsoft/terminal/issues/1205
|
||||
[#1142]: https://github.com/microsoft/terminal/pull/1349
|
||||
[#2046]: https://github.com/microsoft/terminal/issues/2046
|
||||
[#1571]: https://github.com/microsoft/terminal/issues/1571
|
||||
[#3879]: https://github.com/microsoft/terminal/issues/3879
|
||||
[#5280]: https://github.com/microsoft/terminal/pull/5280
|
110
doc/terminal-v2-roadmap.md
Normal file
|
@ -0,0 +1,110 @@
|
|||
# Terminal 2.0 Roadmap
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines the roadmap towards delivering Windows Terminal 2.0 by Spring 2021.
|
||||
|
||||
|
||||
## Milestones
|
||||
|
||||
The Windows Terminal project is engineered and delivered as a set of 4-week milestones. New features will go into [Windows Terminal Preview](https://aka.ms/terminal-preview) first, then a month after they've been in Preview, those features will move into [Windows Terminal](https://aka.ms/terminal).
|
||||
|
||||
| Duration | Activity | Releases |
|
||||
| --- | --- | --- |
|
||||
| 2 weeks | Dev Work<br/> <ul><li>Fixes / Features for future Windows Releases</li><li>Fixes / Features for Windows Terminal</li></ul> | Release to Internal Selfhosters at end of week 2 |
|
||||
| 1 week | Quality & Stability<br/> <ul><li>Bug Fixes</li><li>Perf & Stability</li><li>UI Polish</li><li>Tests</li><li>etc.</li></ul>| Push to Microsoft Store at end of week 3 |
|
||||
| 1 week | Release <br/> <ul><li>Available from [Microsoft Store](https://aka.ms/terminal) & [GitHub Releases](https://github.com/microsoft/terminal/releases)</li><li>Release Notes & Announcement Blog published</li><li>Engineering System Maintenance</li><li>Community Engagement</li><li>Docs</li><li>Future Milestone Planning</li></ul> | Release available from Microsoft Store & GitHub Releases |
|
||||
|
||||
## Terminal Roadmap / Timeline
|
||||
|
||||
Below is the schedule for when milestones will be included in release builds of Windows Terminal and Windows Terminal Preview. The dates are rough estimates and are subject to change.
|
||||
|
||||
| Milestone End Date | Milestone Name | Preview Release Blog Post |
|
||||
| ------------------ | -------------- | ------------------------- |
|
||||
| 2020-06-18 | [1.1] in Windows Terminal Preview | [Windows Terminal Preview 1.1 Release](https://devblogs.microsoft.com/commandline/windows-terminal-preview-1-1-release/) |
|
||||
| 2020-07-31 | [1.2] in Windows Terminal Preview<br>[1.1] in Windows Terminal | |
|
||||
| 2020-08-31 | 1.3 in Windows Terminal Preview<br>[1.2] in Windows Terminal | |
|
||||
| 2020-09-30 | 1.4 in Windows Terminal Preview<br>1.3 in Windows Terminal | |
|
||||
| 2020-10-31 | 1.5 in Windows Terminal Preview<br>1.4 in Windows Terminal | |
|
||||
| 2020-11-30 | 1.6 in Windows Terminal Preview<br>1.5 in Windows Terminal | |
|
||||
| 2020-12-31 | 1.7 in Windows Terminal Preview<br>1.6 in Windows Terminal | |
|
||||
| 2021-01-31 | 1.8 in Windows Terminal Preview<br>1.7 in Windows Terminal | |
|
||||
| 2021-02-28 | 1.9 in Windows Terminal Preview<br>1.8 in Windows Terminal | |
|
||||
| 2021-03-31 | 1.10 in Windows Terminal Preview<br>1.9 in Windows Terminal | |
|
||||
| 2021-04-30 | 2.0 RC in Windows Terminal Preview<br>2.0 RC in Windows Terminal | |
|
||||
| 2021-05-31 | [2.0] in Windows Terminal Preview<br>[2.0] in Windows Terminal | |
|
||||
|
||||
## Issue Triage & Prioritization
|
||||
|
||||
Incoming issues/asks/etc. are triaged several times a week, labeled appropriately, and assigned to a milestone in priority order:
|
||||
|
||||
* P0 (serious crashes, data loss, etc.) issues are scheduled to be dealt with ASAP
|
||||
* P1/2 issues/features/asks assigned to the current or future milestone, or to the [Terminal 2.0 milestone](https://github.com/microsoft/terminal/milestone/22) for future assignment, if required to deliver a 2.0 feature
|
||||
* Issues/features/asks not on our list of 2.0 features are assigned to the [Terminal Backlog](https://github.com/microsoft/terminal/milestone/7) for subsequent triage, prioritization & scheduling.
|
||||
|
||||
## 2.0 Scenarios
|
||||
|
||||
The following are a list of the key scenarios we're aiming to deliver for Terminal 2.0.
|
||||
|
||||
> 👉 Note: There are many other features that don't fit within 2.0, but will be re-assessed and prioritized for 3.0, the plan for which will be published in 2021.
|
||||
|
||||
| Priority\* | Scenario | Description/Notes |
|
||||
| ---------- | -------- | ----------------- |
|
||||
| 0 | Settings UI | A user interface that connects to settings.json. This provides a way for people to edit their settings without having to edit a JSON file.<br><br>Issue: [#1564] |
|
||||
| 0 | Command palette | A popup menu to list possible actions and commands.<br><br>Issues: [#5400], [#2046]<br>Spec: [#2193] |
|
||||
| 1 | Tab tear-off | The ability to tear a tab out of the current window and spawn a new window or attach it to a separate window.<br><br>Issue: [#1256]<br>Spec: [#2080] |
|
||||
| 1 | Clickable links | Hyperlinking any links that appear in the text buffer. When clicking on the link, the link will open in your default browser.<br><br>Issue: [#574] |
|
||||
| 1 | Default terminal | If a command-line application is spawned, it should open in Windows Terminal (if installed) or your preferred terminal<br><br>Issue: [#492]<br>Spec: [#2080] |
|
||||
| 1 | Overall theme support | Tab coloring, title bar coloring, pane border coloring, pane border width, definition of what makes a theme<br><br>Issue: [#3327]<br>Spec: [#5772] |
|
||||
| 1 | Open tab as admin/other user | Open tab in existing Windows Terminal instance as admin (if Terminal was run unelevated) or as another user.<br><br>Issue: [#5000] |
|
||||
| 1 | Traditional opacity | Have a transparent background without the acrylic blur.<br><br>Issue: [#603] |
|
||||
| 2 | SnapOnOutput, scroll lock | Pause output or scrolling on click.<br><br>Issue: [#980]<br>Spec: [#2529]<br>Implementation: [#6062] |
|
||||
| 2 | Infinite scrollback | Have an infinite history for the text buffer.<br><br>Issue: [#1410] |
|
||||
| 2 | Pane management | All issues listed out in the original issue. Some features include pane resizing with mouse, pane zooming, and opening a pane by prompting which profile to use.<br><br>Issue: [#1000] |
|
||||
| 2 | Theme marketplace | Marketplace for creation and distribution of themes.<br>Dependent on overall theming |
|
||||
| 2 | Jump list | Show profiles from task bar (on right click)/start menu.<br><br>Issue: [#576] |
|
||||
| 2 | Open with multiple tabs | A setting that allows Windows Terminal to launch with a specific tab configuration (not using only command line arguments).<br><br>Issue: [#756] |
|
||||
| 3 | Open in Windows Terminal | Functionality to right click on a file or folder and select Open in Windows Terminal.<br><br>Issue: [#1060]<br>Implementation: [#6100] |
|
||||
| 3 | Session restoration | Launch Windows Terminal and the previous session is restored with the proper tab and pane configuration and starting directories.<br><br>Issues: [#961], [#960], [#766] |
|
||||
| 3 | Quake mode | Provide a quick launch terminal that appears and disappears when a hotkey is pressed.<br><br>Issue: [#653] |
|
||||
| 3 | Settings migration infrastructure | Migrate people's settings without breaking them. Hand-in-hand with settings UI. |
|
||||
| 3 | Pointer bindings | Provide settings that can be bound to the mouse.<br><br>Issue: [#1553] |
|
||||
|
||||
Feature Notes:
|
||||
|
||||
\* Feature Priorities:
|
||||
|
||||
0. Mandatory <br/>
|
||||
1. Optimal <br/>
|
||||
2. Optional / Stretch-goal <br/>
|
||||
|
||||
[1.1]: https://github.com/microsoft/terminal/milestone/24
|
||||
[1.2]: https://github.com/microsoft/terminal/milestone/25
|
||||
[2.0]: https://github.com/microsoft/terminal/milestone/22
|
||||
[#1564]: https://github.com/microsoft/terminal/issues/1564
|
||||
[#5400]: https://github.com/microsoft/terminal/issues/5400
|
||||
[#2046]: https://github.com/microsoft/terminal/issues/2046
|
||||
[#2193]: https://github.com/microsoft/terminal/pull/2193
|
||||
[#1256]: https://github.com/microsoft/terminal/issues/1256
|
||||
[#2080]: https://github.com/microsoft/terminal/pull/2080
|
||||
[#574]: https://github.com/microsoft/terminal/issues/574
|
||||
[#492]: https://github.com/microsoft/terminal/issues/492
|
||||
[#2080]: https://github.com/microsoft/terminal/pull/2080
|
||||
[#3327]: https://github.com/microsoft/terminal/issues/3327
|
||||
[#5772]: https://github.com/microsoft/terminal/pull/5772
|
||||
[#5000]: https://github.com/microsoft/terminal/issues/5000
|
||||
[#603]: https://github.com/microsoft/terminal/issues/603
|
||||
[#980]: https://github.com/microsoft/terminal/issues/980
|
||||
[#2529]: https://github.com/microsoft/terminal/pull/2529
|
||||
[#6062]: https://github.com/microsoft/terminal/pull/6062
|
||||
[#1410]: https://github.com/microsoft/terminal/issues/1410
|
||||
[#1000]: https://github.com/microsoft/terminal/issues/1000
|
||||
[#576]: https://github.com/microsoft/terminal/issues/576
|
||||
[#756]: https://github.com/microsoft/terminal/issues/756
|
||||
[#1060]: https://github.com/microsoft/terminal/issues/1060
|
||||
[#6100]: https://github.com/microsoft/terminal/pull/6100
|
||||
[#961]: https://github.com/microsoft/terminal/issues/961
|
||||
[#960]: https://github.com/microsoft/terminal/issues/960
|
||||
[#766]: https://github.com/microsoft/terminal/issues/766
|
||||
[#653]: https://github.com/microsoft/terminal/issues/653
|
||||
[#1553]: https://github.com/microsoft/terminal/issues/1553
|
|
@ -37,7 +37,7 @@ namespace Samples.Terminal
|
|||
internal ReadConsoleInputStream(HFILE handle,
|
||||
BlockingCollection<Kernel32.INPUT_RECORD> nonKeyEvents)
|
||||
{
|
||||
Debug.Assert(handle.IsInvalid == false, "handle.IsInvalid == false");
|
||||
Debug.Assert(!handle.IsInvalid, "handle.IsInvalid == false");
|
||||
|
||||
_handle = handle.DangerousGetHandle();
|
||||
_nonKeyEvents = nonKeyEvents;
|
||||
|
@ -111,7 +111,7 @@ namespace Samples.Terminal
|
|||
if (record.EventType == Kernel32.EVENT_TYPE.KEY_EVENT)
|
||||
{
|
||||
// skip key up events - if not, every key will be duped in the stream
|
||||
if (record.Event.KeyEvent.bKeyDown == false) continue;
|
||||
if (!record.Event.KeyEvent.bKeyDown) continue;
|
||||
|
||||
// pack ucs-2/utf-16le/unicode chars into position in our byte[] buffer.
|
||||
var glyph = (ushort) record.Event.KeyEvent.uChar;
|
||||
|
|
|
@ -264,15 +264,16 @@ void ATTR_ROW::ReplaceAttrs(const TextAttribute& toBeReplacedAttr, const TextAtt
|
|||
{
|
||||
// First we try to find the run where the insertion happens, using lowerBound and upperBound to track
|
||||
// where we are currently at.
|
||||
const auto begin = _list.begin();
|
||||
size_t lowerBound = 0;
|
||||
size_t upperBound = 0;
|
||||
for (size_t i = 0; i < _list.size(); i++)
|
||||
{
|
||||
upperBound += _list.at(i).GetLength();
|
||||
const auto curr = begin + i;
|
||||
upperBound += curr->GetLength();
|
||||
|
||||
if (iStart >= lowerBound && iStart < upperBound)
|
||||
{
|
||||
const auto curr = std::next(_list.begin(), i);
|
||||
|
||||
// The run that we try to insert into has the same color as the new one.
|
||||
// e.g.
|
||||
// AAAAABBBBBBBCCC
|
||||
|
@ -385,7 +386,7 @@ void ATTR_ROW::ReplaceAttrs(const TextAttribute& toBeReplacedAttr, const TextAtt
|
|||
// fact that an existing piece of the run was split in half (to hold the latter half).
|
||||
const size_t cNewRun = _list.size() + newAttrs.size() + 1;
|
||||
std::vector<TextAttributeRun> newRun;
|
||||
newRun.resize(cNewRun);
|
||||
newRun.reserve(cNewRun);
|
||||
|
||||
// We will start analyzing from the beginning of our existing run.
|
||||
// Use some pointers to keep track of where we are in walking through our runs.
|
||||
|
@ -393,10 +394,9 @@ void ATTR_ROW::ReplaceAttrs(const TextAttribute& toBeReplacedAttr, const TextAtt
|
|||
// Get the existing run that we'll be updating/manipulating.
|
||||
const auto existingRun = _list.begin();
|
||||
auto pExistingRunPos = existingRun;
|
||||
const auto pExistingRunEnd = existingRun + _list.size();
|
||||
const auto pExistingRunEnd = _list.end();
|
||||
auto pInsertRunPos = newAttrs.begin();
|
||||
size_t cInsertRunRemaining = newAttrs.size();
|
||||
auto pNewRunPos = newRun.begin();
|
||||
size_t iExistingRunCoverage = 0;
|
||||
|
||||
// Copy the existing run into the new buffer up to the "start index" where the new run will be injected.
|
||||
|
@ -410,7 +410,7 @@ void ATTR_ROW::ReplaceAttrs(const TextAttribute& toBeReplacedAttr, const TextAtt
|
|||
iExistingRunCoverage += pExistingRunPos->GetLength();
|
||||
|
||||
// Copy it to the new run buffer and advance both pointers.
|
||||
*pNewRunPos++ = *pExistingRunPos++;
|
||||
newRun.push_back(*pExistingRunPos++);
|
||||
}
|
||||
|
||||
// When we get to this point, we've copied full segments from the original existing run
|
||||
|
@ -429,12 +429,8 @@ void ATTR_ROW::ReplaceAttrs(const TextAttribute& toBeReplacedAttr, const TextAtt
|
|||
// We need to fix this up below so it says G2 instead to leave room for the Y3 to fit in
|
||||
// the new/final run.
|
||||
|
||||
// Copying above advanced the pointer to an empty cell beyond what we copied.
|
||||
// Back up one cell so we can manipulate the final item we copied from the existing run to the new run.
|
||||
pNewRunPos--;
|
||||
|
||||
// Fetch out the length so we can fix it up based on the below conditions.
|
||||
size_t length = pNewRunPos->GetLength();
|
||||
size_t length = newRun.back().GetLength();
|
||||
|
||||
// If we've covered more cells already than the start of the attributes to be inserted...
|
||||
if (iExistingRunCoverage > iStart)
|
||||
|
@ -449,7 +445,7 @@ void ATTR_ROW::ReplaceAttrs(const TextAttribute& toBeReplacedAttr, const TextAtt
|
|||
// Now we're still on that "last cell copied" into the new run.
|
||||
// If the color of that existing copied cell matches the color of the first segment
|
||||
// of the run we're about to insert, we can just increment the length to extend the coverage.
|
||||
if (pNewRunPos->GetAttributes() == pInsertRunPos->GetAttributes())
|
||||
if (newRun.back().GetAttributes() == pInsertRunPos->GetAttributes())
|
||||
{
|
||||
length += pInsertRunPos->GetLength();
|
||||
|
||||
|
@ -460,18 +456,11 @@ void ATTR_ROW::ReplaceAttrs(const TextAttribute& toBeReplacedAttr, const TextAtt
|
|||
}
|
||||
|
||||
// We're done manipulating the length. Store it back.
|
||||
pNewRunPos->SetLength(length);
|
||||
|
||||
// Now that we're done adjusting the last copied item, advance the pointer into a fresh/blank
|
||||
// part of the new run array.
|
||||
pNewRunPos++;
|
||||
newRun.back().SetLength(length);
|
||||
}
|
||||
|
||||
// Bulk copy the majority (or all, depending on circumstance) of the insert run into the final run buffer.
|
||||
std::copy_n(pInsertRunPos, cInsertRunRemaining, pNewRunPos);
|
||||
|
||||
// Advance the new run pointer into the position just after everything we copied.
|
||||
pNewRunPos += cInsertRunRemaining;
|
||||
std::copy_n(pInsertRunPos, cInsertRunRemaining, std::back_inserter(newRun));
|
||||
|
||||
// We're technically done with the insert run now and have 0 remaining, but won't bother updating its pointers
|
||||
// and counts any further because we won't use them.
|
||||
|
@ -488,9 +477,6 @@ void ATTR_ROW::ReplaceAttrs(const TextAttribute& toBeReplacedAttr, const TextAtt
|
|||
// If we still have original existing run cells remaining, copy them into the final new run.
|
||||
if (pExistingRunPos != pExistingRunEnd || iExistingRunCoverage != (iEnd + 1))
|
||||
{
|
||||
// Back up one cell so we can inspect the most recent item copied into the new run for optimizations.
|
||||
pNewRunPos--;
|
||||
|
||||
// We advanced the existing run pointer and its count to on or past the end of what the insertion run filled in.
|
||||
// If this ended up being past the end of what the insertion run covers, we have to account for the cells after
|
||||
// the insertion run but before the next piece of the original existing run.
|
||||
|
@ -514,11 +500,11 @@ void ATTR_ROW::ReplaceAttrs(const TextAttribute& toBeReplacedAttr, const TextAtt
|
|||
// This case is slightly off from the example above. This case is for if the B2 above was actually Y2.
|
||||
// That Y2 from the existing run is the same color as the Y2 we just filled a few columns left in the final run
|
||||
// so we can just adjust the final run's column count instead of adding another segment here.
|
||||
if (pNewRunPos->GetAttributes() == pExistingRunPos->GetAttributes())
|
||||
if (newRun.back().GetAttributes() == pExistingRunPos->GetAttributes())
|
||||
{
|
||||
size_t length = pNewRunPos->GetLength();
|
||||
size_t length = newRun.back().GetLength();
|
||||
length += (iExistingRunCoverage - (iEnd + 1));
|
||||
pNewRunPos->SetLength(length);
|
||||
newRun.back().SetLength(length);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -526,14 +512,14 @@ void ATTR_ROW::ReplaceAttrs(const TextAttribute& toBeReplacedAttr, const TextAtt
|
|||
// its length for the discrepancy in columns not yet covered by the final/new run.
|
||||
|
||||
// Move forward to a blank spot in the new run
|
||||
pNewRunPos++;
|
||||
newRun.emplace_back();
|
||||
|
||||
// Copy the existing run's color information to the new run
|
||||
pNewRunPos->SetAttributes(pExistingRunPos->GetAttributes());
|
||||
newRun.back().SetAttributes(pExistingRunPos->GetAttributes());
|
||||
|
||||
// Adjust the length of that copied color to cover only the reduced number of columns needed
|
||||
// now that some have been replaced by the insert run.
|
||||
pNewRunPos->SetLength(iExistingRunCoverage - (iEnd + 1));
|
||||
newRun.back().SetLength(iExistingRunCoverage - (iEnd + 1));
|
||||
}
|
||||
|
||||
// Now that we're done recovering a piece of the existing run we skipped, move the pointer forward again.
|
||||
|
@ -551,34 +537,25 @@ void ATTR_ROW::ReplaceAttrs(const TextAttribute& toBeReplacedAttr, const TextAtt
|
|||
// New Run desired when done = R3 -> B7
|
||||
// Existing run pointer is on B2.
|
||||
// We want to merge the 2 from the B2 into the B5 so we get B7.
|
||||
else if (pNewRunPos->GetAttributes() == pExistingRunPos->GetAttributes())
|
||||
else if (newRun.back().GetAttributes() == pExistingRunPos->GetAttributes())
|
||||
{
|
||||
// Add the value from the existing run into the current new run position.
|
||||
size_t length = pNewRunPos->GetLength();
|
||||
size_t length = newRun.back().GetLength();
|
||||
length += pExistingRunPos->GetLength();
|
||||
pNewRunPos->SetLength(length);
|
||||
newRun.back().SetLength(length);
|
||||
|
||||
// Advance the existing run position since we consumed its value and merged it in.
|
||||
pExistingRunPos++;
|
||||
}
|
||||
|
||||
// OK. We're done inspecting the most recently copied cell for optimizations.
|
||||
pNewRunPos++;
|
||||
|
||||
// Now bulk copy any segments left in the original existing run
|
||||
if (pExistingRunPos < pExistingRunEnd)
|
||||
{
|
||||
std::copy_n(pExistingRunPos, (pExistingRunEnd - pExistingRunPos), pNewRunPos);
|
||||
|
||||
// Fix up the end pointer so we know where we are for counting how much of the new run's memory space we used.
|
||||
pNewRunPos += (pExistingRunEnd - pExistingRunPos);
|
||||
std::copy_n(pExistingRunPos, (pExistingRunEnd - pExistingRunPos), std::back_inserter(newRun));
|
||||
}
|
||||
}
|
||||
|
||||
// OK, phew. We're done. Now we just need to free the existing run, store the new run in its place,
|
||||
// and update the count for the correct length of the new run now that we've filled it up.
|
||||
|
||||
newRun.erase(pNewRunPos, newRun.end());
|
||||
// OK, phew. We're done. Now we just need to free the existing run and store the new run in its place.
|
||||
_list.swap(newRun);
|
||||
|
||||
return S_OK;
|
||||
|
|
|
@ -5,16 +5,26 @@
|
|||
#include "TextAttribute.hpp"
|
||||
#include "../../inc/conattrs.hpp"
|
||||
|
||||
// Routine Description:
|
||||
// - Returns a WORD with legacy-style attributes for this textattribute.
|
||||
// Parameters:
|
||||
// - defaultAttributes: the attribute values to be used for default colors.
|
||||
// Return value:
|
||||
// - a WORD with legacy-style attributes for this textattribute.
|
||||
WORD TextAttribute::GetLegacyAttributes(const WORD defaultAttributes) const noexcept
|
||||
{
|
||||
const BYTE fgIndex = _foreground.GetLegacyIndex(defaultAttributes & FG_ATTRS);
|
||||
const BYTE bgIndex = _background.GetLegacyIndex((defaultAttributes & BG_ATTRS) >> 4);
|
||||
const WORD metaAttrs = _wAttrLegacy & META_ATTRS;
|
||||
const bool brighten = _foreground.IsIndex16() && IsBold();
|
||||
return fgIndex | (bgIndex << 4) | metaAttrs | (brighten ? FOREGROUND_INTENSITY : 0);
|
||||
}
|
||||
|
||||
bool TextAttribute::IsLegacy() const noexcept
|
||||
{
|
||||
return _foreground.IsLegacy() && _background.IsLegacy();
|
||||
}
|
||||
|
||||
bool TextAttribute::IsHighColor() const noexcept
|
||||
{
|
||||
return _foreground.IsHighColor() || _background.IsHighColor();
|
||||
}
|
||||
|
||||
// Arguments:
|
||||
// - None
|
||||
// Return Value:
|
||||
|
@ -241,21 +251,6 @@ void TextAttribute::SetDefaultBackground() noexcept
|
|||
_background = TextColor();
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Returns true if this attribute indicates its foreground is the "default"
|
||||
// foreground. Its _rgbForeground will contain the actual value of the
|
||||
// default foreground. If the default colors are ever changed, this method
|
||||
// should be used to identify attributes with the default fg value, and
|
||||
// update them accordingly.
|
||||
// Arguments:
|
||||
// - <none>
|
||||
// Return Value:
|
||||
// - true iff this attribute indicates it's the "default" foreground color.
|
||||
bool TextAttribute::ForegroundIsDefault() const noexcept
|
||||
{
|
||||
return _foreground.IsDefault();
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Returns true if this attribute indicates its background is the "default"
|
||||
// background. Its _rgbBackground will contain the actual value of the
|
||||
|
|
|
@ -59,38 +59,7 @@ public:
|
|||
{
|
||||
}
|
||||
|
||||
WORD GetLegacyAttributes() const noexcept
|
||||
{
|
||||
const BYTE fg = (_foreground.GetIndex() & FG_ATTRS);
|
||||
const BYTE bg = (_background.GetIndex() << 4) & BG_ATTRS;
|
||||
const WORD meta = (_wAttrLegacy & META_ATTRS);
|
||||
const bool brighten = _foreground.IsIndex16() && IsBold();
|
||||
return (fg | bg | meta) | (brighten ? FOREGROUND_INTENSITY : 0);
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Returns a WORD with legacy-style attributes for this textattribute.
|
||||
// If either the foreground or background of this textattribute is not
|
||||
// a legacy attribute, then instead use the provided default index as
|
||||
// the value for that component.
|
||||
// Arguments:
|
||||
// - defaultFgIndex: the BYTE to use as the index for the foreground, should
|
||||
// the foreground not be a legacy style attribute.
|
||||
// - defaultBgIndex: the BYTE to use as the index for the background, should
|
||||
// the background not be a legacy style attribute.
|
||||
// Return Value:
|
||||
// - a WORD with legacy-style attributes for this textattribute.
|
||||
WORD GetLegacyAttributes(const BYTE defaultFgIndex,
|
||||
const BYTE defaultBgIndex) const noexcept
|
||||
{
|
||||
const BYTE fgIndex = _foreground.IsLegacy() ? _foreground.GetIndex() : defaultFgIndex;
|
||||
const BYTE bgIndex = _background.IsLegacy() ? _background.GetIndex() : defaultBgIndex;
|
||||
const BYTE fg = (fgIndex & FG_ATTRS);
|
||||
const BYTE bg = (bgIndex << 4) & BG_ATTRS;
|
||||
const WORD meta = (_wAttrLegacy & META_ATTRS);
|
||||
const bool brighten = _foreground.IsIndex16() && IsBold();
|
||||
return (fg | bg | meta) | (brighten ? FOREGROUND_INTENSITY : 0);
|
||||
}
|
||||
WORD GetLegacyAttributes(const WORD defaultAttributes = 0x07) const noexcept;
|
||||
|
||||
COLORREF CalculateRgbForeground(std::basic_string_view<COLORREF> colorTable,
|
||||
COLORREF defaultFgColor,
|
||||
|
@ -119,7 +88,6 @@ public:
|
|||
friend constexpr bool operator!=(const WORD& legacyAttr, const TextAttribute& attr) noexcept;
|
||||
|
||||
bool IsLegacy() const noexcept;
|
||||
bool IsHighColor() const noexcept;
|
||||
bool IsBold() const noexcept;
|
||||
bool IsItalic() const noexcept;
|
||||
bool IsBlinking() const noexcept;
|
||||
|
@ -149,7 +117,6 @@ public:
|
|||
void SetDefaultForeground() noexcept;
|
||||
void SetDefaultBackground() noexcept;
|
||||
|
||||
bool ForegroundIsDefault() const noexcept;
|
||||
bool BackgroundIsDefault() const noexcept;
|
||||
|
||||
void SetStandardErase() noexcept;
|
||||
|
|
|
@ -4,16 +4,57 @@
|
|||
#include "precomp.h"
|
||||
#include "TextColor.h"
|
||||
|
||||
// clang-format off
|
||||
|
||||
// A table mapping 8-bit RGB colors, in the form RRRGGGBB,
|
||||
// down to one of the 16 colors in the legacy palette.
|
||||
constexpr std::array<BYTE, 256> CompressedRgbToIndex16 = {
|
||||
0, 1, 1, 9, 0, 0, 1, 1, 2, 1, 1, 1, 2, 8, 1, 9,
|
||||
2, 2, 3, 3, 2, 2, 11, 3, 10, 10, 11, 11, 10, 10, 10, 11,
|
||||
0, 5, 1, 1, 0, 0, 1, 1, 8, 1, 1, 1, 2, 8, 1, 9,
|
||||
2, 2, 3, 3, 2, 2, 11, 3, 10, 10, 10, 11, 10, 10, 10, 11,
|
||||
5, 5, 5, 1, 4, 5, 1, 1, 8, 8, 1, 9, 2, 8, 9, 9,
|
||||
2, 2, 3, 3, 2, 2, 11, 3, 10, 10, 11, 11, 10, 10, 10, 11,
|
||||
4, 5, 5, 1, 4, 5, 5, 1, 8, 5, 5, 1, 8, 8, 9, 9,
|
||||
2, 2, 8, 9, 10, 2, 11, 3, 10, 10, 11, 11, 10, 10, 10, 11,
|
||||
4, 13, 5, 5, 4, 13, 5, 5, 4, 13, 13, 13, 6, 8, 13, 9,
|
||||
6, 8, 8, 9, 10, 10, 11, 3, 10, 10, 11, 11, 10, 10, 10, 11,
|
||||
4, 13, 13, 13, 4, 13, 13, 13, 4, 12, 13, 13, 6, 12, 13, 13,
|
||||
6, 6, 8, 9, 6, 6, 7, 7, 10, 14, 14, 7, 10, 10, 14, 11,
|
||||
4, 12, 13, 13, 4, 12, 13, 13, 4, 12, 13, 13, 6, 12, 12, 13,
|
||||
6, 6, 12, 7, 6, 6, 7, 7, 6, 14, 14, 7, 14, 14, 14, 15,
|
||||
12, 12, 13, 13, 12, 12, 13, 13, 12, 12, 12, 13, 12, 12, 12, 13,
|
||||
6, 12, 12, 7, 6, 6, 7, 7, 6, 14, 14, 7, 14, 14, 14, 15
|
||||
};
|
||||
|
||||
// A table mapping indexed colors from the 256-color palette,
|
||||
// down to one of the 16 colors in the legacy palette.
|
||||
constexpr std::array<BYTE, 256> Index256ToIndex16 = {
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
|
||||
0, 1, 1, 1, 9, 9, 2, 1, 1, 1, 1, 1, 2, 2, 3, 3,
|
||||
3, 3, 2, 2, 11, 11, 3, 3, 10, 10, 11, 11, 11, 11, 10, 10,
|
||||
10, 10, 11, 11, 5, 5, 5, 5, 1, 1, 8, 8, 1, 1, 9, 9,
|
||||
2, 2, 3, 3, 3, 3, 2, 2, 11, 11, 3, 3, 10, 10, 11, 11,
|
||||
11, 11, 10, 10, 10, 10, 11, 11, 4, 13, 5, 5, 5, 5, 4, 13,
|
||||
13, 13, 13, 13, 6, 8, 8, 8, 9, 9, 10, 10, 11, 11, 3, 3,
|
||||
10, 10, 11, 11, 11, 11, 10, 10, 10, 10, 11, 11, 4, 13, 13, 13,
|
||||
13, 13, 4, 12, 13, 13, 13, 13, 6, 6, 8, 8, 9, 9, 6, 6,
|
||||
7, 7, 7, 7, 10, 14, 14, 14, 7, 7, 10, 10, 14, 14, 11, 11,
|
||||
4, 12, 13, 13, 13, 13, 4, 12, 13, 13, 13, 13, 6, 6, 12, 12,
|
||||
7, 7, 6, 6, 7, 7, 7, 7, 6, 14, 14, 14, 7, 7, 14, 14,
|
||||
14, 14, 15, 15, 12, 12, 13, 13, 13, 13, 12, 12, 12, 12, 13, 13,
|
||||
6, 12, 12, 12, 7, 7, 6, 6, 7, 7, 7, 7, 6, 14, 14, 14,
|
||||
7, 7, 14, 14, 14, 14, 15, 15, 0, 0, 0, 0, 0, 0, 8, 8,
|
||||
8, 8, 8, 8, 8, 8, 8, 8, 7, 7, 7, 7, 7, 7, 15, 15
|
||||
};
|
||||
|
||||
// clang-format on
|
||||
|
||||
bool TextColor::IsLegacy() const noexcept
|
||||
{
|
||||
return IsIndex16() || (IsIndex256() && _index < 16);
|
||||
}
|
||||
|
||||
bool TextColor::IsHighColor() const noexcept
|
||||
{
|
||||
return IsRgb() || (IsIndex256() && _index >= 16);
|
||||
}
|
||||
|
||||
bool TextColor::IsIndex16() const noexcept
|
||||
{
|
||||
return _meta == ColorType::IsIndex16;
|
||||
|
@ -135,6 +176,37 @@ COLORREF TextColor::GetColor(std::basic_string_view<COLORREF> colorTable,
|
|||
}
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Return a legacy index value that best approximates this color.
|
||||
// Arguments:
|
||||
// - defaultIndex: The index to use for a default color.
|
||||
// Return Value:
|
||||
// - an index into the 16-color table
|
||||
BYTE TextColor::GetLegacyIndex(const BYTE defaultIndex) const noexcept
|
||||
{
|
||||
if (IsDefault())
|
||||
{
|
||||
return defaultIndex;
|
||||
}
|
||||
else if (IsIndex16())
|
||||
{
|
||||
return GetIndex();
|
||||
}
|
||||
else if (IsIndex256())
|
||||
{
|
||||
return Index256ToIndex16.at(GetIndex());
|
||||
}
|
||||
else
|
||||
{
|
||||
// We compress the RGB down to an 8-bit value and use that to
|
||||
// lookup a representative 16-color index from a hard-coded table.
|
||||
const BYTE compressedRgb = (_red & 0b11100000) +
|
||||
((_green >> 3) & 0b00011100) +
|
||||
((_blue >> 6) & 0b00000011);
|
||||
return CompressedRgbToIndex16.at(compressedRgb);
|
||||
}
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Return a COLORREF containing our stored value. Will return garbage if this
|
||||
//attribute is not a RGB attribute.
|
||||
|
|
|
@ -78,7 +78,6 @@ public:
|
|||
friend constexpr bool operator!=(const TextColor& a, const TextColor& b) noexcept;
|
||||
|
||||
bool IsLegacy() const noexcept;
|
||||
bool IsHighColor() const noexcept;
|
||||
bool IsIndex16() const noexcept;
|
||||
bool IsIndex256() const noexcept;
|
||||
bool IsDefault() const noexcept;
|
||||
|
@ -92,6 +91,8 @@ public:
|
|||
const COLORREF defaultColor,
|
||||
const bool brighten) const noexcept;
|
||||
|
||||
BYTE GetLegacyIndex(const BYTE defaultIndex) const noexcept;
|
||||
|
||||
constexpr BYTE GetIndex() const noexcept
|
||||
{
|
||||
return _index;
|
||||
|
|
|
@ -47,7 +47,7 @@ void UnicodeStorage::Erase(const key_type key)
|
|||
// - rowMap - A map of the old row IDs to the new row IDs.
|
||||
// - width - The width of the new row. Remove any items that are beyond the row width.
|
||||
// - Use nullopt if we're not resizing the width of the row, just renumbering the rows.
|
||||
void UnicodeStorage::Remap(const std::map<SHORT, SHORT>& rowMap, const std::optional<SHORT> width)
|
||||
void UnicodeStorage::Remap(const std::unordered_map<SHORT, SHORT>& rowMap, const std::optional<SHORT> width)
|
||||
{
|
||||
// Make a temporary map to hold all the new row positioning
|
||||
std::unordered_map<key_type, mapped_type> newMap;
|
||||
|
|
|
@ -55,7 +55,7 @@ public:
|
|||
|
||||
void Erase(const key_type key);
|
||||
|
||||
void Remap(const std::map<SHORT, SHORT>& rowMap, const std::optional<SHORT> width);
|
||||
void Remap(const std::unordered_map<SHORT, SHORT>& rowMap, const std::optional<SHORT> width);
|
||||
|
||||
private:
|
||||
std::unordered_map<key_type, mapped_type> _map;
|
||||
|
|
|
@ -872,7 +872,7 @@ UnicodeStorage& TextBuffer::GetUnicodeStorage() noexcept
|
|||
// - newRowWidth - Optional new value for the row width.
|
||||
void TextBuffer::_RefreshRowIDs(std::optional<SHORT> newRowWidth)
|
||||
{
|
||||
std::map<SHORT, SHORT> rowMap;
|
||||
std::unordered_map<SHORT, SHORT> rowMap;
|
||||
SHORT i = 0;
|
||||
for (auto& it : _storage)
|
||||
{
|
||||
|
|
|
@ -46,8 +46,9 @@
|
|||
<!-- Resources -->
|
||||
<!-- This resw only defines things that are used in this package's AppxManifest,
|
||||
so it's not in the common resource items. -->
|
||||
<PRIResource Include="Resources\*\Resources.resw" />
|
||||
<PRIResource Include="Resources\en-US\Resources.resw" />
|
||||
<PRIResource Include="Resources\Resources.resw" />
|
||||
<OCResourceDirectory Include="Resources" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- This is picked up by CascadiaResources.build.items. -->
|
||||
|
@ -148,4 +149,6 @@
|
|||
</PropertyGroup>
|
||||
<Error Condition="!Exists('..\..\..\packages\Microsoft.UI.Xaml.2.4.2-prerelease.200604001\build\native\Microsoft.UI.Xaml.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\packages\Microsoft.UI.Xaml.2.4.2-prerelease.200604001\build\native\Microsoft.UI.Xaml.targets'))" />
|
||||
</Target>
|
||||
|
||||
<Import Project="$(SolutionDir)build\rules\CollectWildcardResources.targets" />
|
||||
</Project>
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
Version="0.0.1.0" />
|
||||
|
||||
<Properties>
|
||||
<DisplayName>ms-resource:AppNameDev</DisplayName>
|
||||
<DisplayName>Windows Terminal Dev</DisplayName>
|
||||
<PublisherDisplayName>A Lone Developer</PublisherDisplayName>
|
||||
<Logo>Images\StoreLogo.png</Logo>
|
||||
</Properties>
|
||||
|
@ -82,9 +82,14 @@
|
|||
<desktop5:ItemType Type="Directory">
|
||||
<desktop5:Verb Id="Command1" Clsid="9f156763-7844-4dc4-b2b1-901f640f5155" />
|
||||
</desktop5:ItemType>
|
||||
<!-- Due to a bug in the OS, this doesn't actually work right -
|
||||
we'll get a nullptr in our implementation. So this is disabled
|
||||
temporarily. See MSFT:24623699 for more details.
|
||||
|
||||
<desktop5:ItemType Type="Directory\Background">
|
||||
<desktop5:Verb Id="Command2" Clsid="9f156763-7844-4dc4-b2b1-901f640f5155" />
|
||||
</desktop5:ItemType>
|
||||
-->
|
||||
</desktop4:FileExplorerContextMenus>
|
||||
</desktop4:Extension>
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
Version="0.5.0.0" />
|
||||
|
||||
<Properties>
|
||||
<DisplayName>Windows Terminal (Preview)</DisplayName>
|
||||
<DisplayName>Windows Terminal Preview</DisplayName>
|
||||
<PublisherDisplayName>Microsoft Corporation</PublisherDisplayName>
|
||||
<Logo>Images\StoreLogo.png</Logo>
|
||||
</Properties>
|
||||
|
@ -82,9 +82,15 @@
|
|||
<desktop5:ItemType Type="Directory">
|
||||
<desktop5:Verb Id="Command1" Clsid="9f156763-7844-4dc4-b2b1-901f640f5155" />
|
||||
</desktop5:ItemType>
|
||||
<!-- Due to a bug in the OS, this doesn't actually work right -
|
||||
we'll get a nullptr in our implementation. So this is disabled
|
||||
temporarily. See MSFT:24623699 for more details.
|
||||
|
||||
<desktop5:ItemType Type="Directory\Background">
|
||||
<desktop5:Verb Id="Command2" Clsid="9f156763-7844-4dc4-b2b1-901f640f5155" />
|
||||
</desktop5:ItemType>
|
||||
-->
|
||||
|
||||
</desktop4:FileExplorerContextMenus>
|
||||
</desktop4:Extension>
|
||||
|
||||
|
|
|
@ -83,9 +83,14 @@
|
|||
<desktop5:ItemType Type="Directory">
|
||||
<desktop5:Verb Id="Command1" Clsid="9f156763-7844-4dc4-b2b1-901f640f5155" />
|
||||
</desktop5:ItemType>
|
||||
<!-- Due to a bug in the OS, this doesn't actually work right -
|
||||
we'll get a nullptr in our implementation. So this is disabled
|
||||
temporarily. See MSFT:24623699 for more details.
|
||||
|
||||
<desktop5:ItemType Type="Directory\Background">
|
||||
<desktop5:Verb Id="Command2" Clsid="9f156763-7844-4dc4-b2b1-901f640f5155" />
|
||||
</desktop5:ItemType>
|
||||
-->
|
||||
</desktop4:FileExplorerContextMenus>
|
||||
</desktop4:Extension>
|
||||
|
||||
|
|
|
@ -121,18 +121,18 @@
|
|||
<value>Windows Terminal</value>
|
||||
</data>
|
||||
<data name="AppNameDev" xml:space="preserve">
|
||||
<value>Windows Terminal (Dev Build)</value>
|
||||
<value>Windows Terminal Dev</value>
|
||||
</data>
|
||||
<data name="AppNamePre" xml:space="preserve">
|
||||
<value>Windows Terminal (Preview)</value>
|
||||
<value>Windows Terminal Preview</value>
|
||||
</data>
|
||||
<data name="AppShortName" xml:space="preserve">
|
||||
<value>Terminal</value>
|
||||
</data>
|
||||
<data name="AppShortNameDev" xml:space="preserve">
|
||||
<value>Terminal (Dev)</value>
|
||||
<value>Terminal Dev</value>
|
||||
</data>
|
||||
<data name="AppShortNamePre" xml:space="preserve">
|
||||
<value>Terminal (Preview)</value>
|
||||
<value>Terminal Preview</value>
|
||||
</data>
|
||||
</root>
|
|
@ -123,27 +123,27 @@
|
|||
|
||||
<Import Project="$(OpenConsoleDir)\packages\Microsoft.UI.Xaml.2.4.2-prerelease.200604001\build\native\Microsoft.UI.Xaml.targets" Condition="Exists('$(OpenConsoleDir)\packages\Microsoft.UI.Xaml.2.4.2-prerelease.200604001\build\native\Microsoft.UI.Xaml.targets')" />
|
||||
|
||||
<!-- Use this to auto-find all the dll's that TerminalConnection produces. We
|
||||
don't roll these up automatically, so we'll need to copy them manually
|
||||
(below)
|
||||
|
||||
The dependencies from TerminalConnection get rolled up in the
|
||||
GetPackagingOutputs step, when it produces the "appx recipe". This means
|
||||
they only show up when the build produces an AppX\ folder for either running
|
||||
or packaging.
|
||||
|
||||
It is literally impossible to produce an AppX\ folder using MSBuild without
|
||||
packaging an appx, and we don't want to do that anyway, so we use these copy
|
||||
rules instead.
|
||||
-->
|
||||
<ItemGroup>
|
||||
<TerminalConnectionDlls Include="$(_CppWinrtBinRoot)\TerminalConnection\*.dll"/>
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="$(OpenConsoleDir)\src\common.build.post.props" />
|
||||
|
||||
<Target Name="AfterBuild">
|
||||
|
||||
<!-- Use this to auto-find all the dll's that TerminalConnection produces. We
|
||||
don't roll these up automatically, so we'll need to copy them manually
|
||||
(below)
|
||||
|
||||
The dependencies from TerminalConnection get rolled up in the
|
||||
GetPackagingOutputs step, when it produces the "appx recipe". This means
|
||||
they only show up when the build produces an AppX\ folder for either running
|
||||
or packaging.
|
||||
|
||||
It is literally impossible to produce an AppX\ folder using MSBuild without
|
||||
packaging an appx, and we don't want to do that anyway, so we use these copy
|
||||
rules instead.
|
||||
-->
|
||||
<ItemGroup>
|
||||
<TerminalConnectionDlls Include="$(_CppWinrtBinRoot)\TerminalConnection\*.dll"/>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Copy the AppxManifest.xml to another file, because when TAEF is
|
||||
deploying the app, it'll delete the AppxManifest.xml file from this
|
||||
directory when it tries to clean up after itself. -->
|
||||
|
|
|
@ -85,7 +85,11 @@ try
|
|||
}
|
||||
return DefWindowProc(hwnd, uMsg, wParam, lParam);
|
||||
}
|
||||
CATCH_LOG()
|
||||
catch (...)
|
||||
{
|
||||
LOG_CAUGHT_EXCEPTION();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool RegisterTermClass(HINSTANCE hInstance) noexcept
|
||||
{
|
||||
|
@ -557,11 +561,11 @@ catch (...)
|
|||
return false;
|
||||
}
|
||||
|
||||
void HwndTerminal::_SendKeyEvent(WORD vkey, WORD scanCode) noexcept
|
||||
void HwndTerminal::_SendKeyEvent(WORD vkey, WORD scanCode, bool keyDown) noexcept
|
||||
try
|
||||
{
|
||||
const auto flags = getControlKeyState();
|
||||
_terminal->SendKeyEvent(vkey, scanCode, flags);
|
||||
_terminal->SendKeyEvent(vkey, scanCode, flags, keyDown);
|
||||
}
|
||||
CATCH_LOG();
|
||||
|
||||
|
@ -590,10 +594,10 @@ try
|
|||
}
|
||||
CATCH_LOG();
|
||||
|
||||
void _stdcall TerminalSendKeyEvent(void* terminal, WORD vkey, WORD scanCode)
|
||||
void _stdcall TerminalSendKeyEvent(void* terminal, WORD vkey, WORD scanCode, bool keyDown)
|
||||
{
|
||||
const auto publicTerminal = static_cast<HwndTerminal*>(terminal);
|
||||
publicTerminal->_SendKeyEvent(vkey, scanCode);
|
||||
publicTerminal->_SendKeyEvent(vkey, scanCode, keyDown);
|
||||
}
|
||||
|
||||
void _stdcall TerminalSendCharEvent(void* terminal, wchar_t ch, WORD scanCode)
|
||||
|
|
|
@ -34,7 +34,7 @@ __declspec(dllexport) bool _stdcall TerminalIsSelectionActive(void* terminal);
|
|||
__declspec(dllexport) void _stdcall DestroyTerminal(void* terminal);
|
||||
__declspec(dllexport) void _stdcall TerminalSetTheme(void* terminal, TerminalTheme theme, LPCWSTR fontFamily, short fontSize, int newDpi);
|
||||
__declspec(dllexport) void _stdcall TerminalRegisterWriteCallback(void* terminal, const void __stdcall callback(wchar_t*));
|
||||
__declspec(dllexport) void _stdcall TerminalSendKeyEvent(void* terminal, WORD vkey, WORD scanCode);
|
||||
__declspec(dllexport) void _stdcall TerminalSendKeyEvent(void* terminal, WORD vkey, WORD scanCode, bool keyDown);
|
||||
__declspec(dllexport) void _stdcall TerminalSendCharEvent(void* terminal, wchar_t ch, WORD scanCode);
|
||||
__declspec(dllexport) void _stdcall TerminalBlinkCursor(void* terminal);
|
||||
__declspec(dllexport) void _stdcall TerminalSetCursorVisible(void* terminal, const bool visible);
|
||||
|
@ -92,7 +92,7 @@ private:
|
|||
friend void _stdcall TerminalClearSelection(void* terminal);
|
||||
friend const wchar_t* _stdcall TerminalGetSelection(void* terminal);
|
||||
friend bool _stdcall TerminalIsSelectionActive(void* terminal);
|
||||
friend void _stdcall TerminalSendKeyEvent(void* terminal, WORD vkey, WORD scanCode);
|
||||
friend void _stdcall TerminalSendKeyEvent(void* terminal, WORD vkey, WORD scanCode, bool keyDown);
|
||||
friend void _stdcall TerminalSendCharEvent(void* terminal, wchar_t ch, WORD scanCode);
|
||||
friend void _stdcall TerminalSetTheme(void* terminal, TerminalTheme theme, LPCWSTR fontFamily, short fontSize, int newDpi);
|
||||
friend void _stdcall TerminalBlinkCursor(void* terminal);
|
||||
|
@ -115,7 +115,7 @@ private:
|
|||
bool _CanSendVTMouseInput() const noexcept;
|
||||
bool _SendMouseEvent(UINT uMsg, WPARAM wParam, LPARAM lParam) noexcept;
|
||||
|
||||
void _SendKeyEvent(WORD vkey, WORD scanCode) noexcept;
|
||||
void _SendKeyEvent(WORD vkey, WORD scanCode, bool keyDown) noexcept;
|
||||
void _SendCharEvent(wchar_t ch, WORD scanCode) noexcept;
|
||||
|
||||
// Inherited via IControlAccessibilityInfo
|
||||
|
|
|
@ -1,11 +1,212 @@
|
|||
#include "pch.h"
|
||||
#include "ActionArgs.h"
|
||||
#include "ActionAndArgs.h"
|
||||
#include "ActionAndArgs.g.cpp"
|
||||
|
||||
// We define everything necessary for the ActionAndArgs class in the header, but
|
||||
// we still need this file to compile the ActionAndArgs.g.cpp file, and we can't
|
||||
// just include that file in the header.
|
||||
static constexpr std::string_view ActionKey{ "action" };
|
||||
|
||||
// This key is reserved to remove a keybinding, instead of mapping it to an action.
|
||||
static constexpr std::string_view UnboundKey{ "unbound" };
|
||||
|
||||
static constexpr std::string_view CopyTextKey{ "copy" };
|
||||
static constexpr std::string_view PasteTextKey{ "paste" };
|
||||
static constexpr std::string_view OpenNewTabDropdownKey{ "openNewTabDropdown" };
|
||||
static constexpr std::string_view DuplicateTabKey{ "duplicateTab" };
|
||||
static constexpr std::string_view NewTabKey{ "newTab" };
|
||||
static constexpr std::string_view NewWindowKey{ "newWindow" };
|
||||
static constexpr std::string_view CloseWindowKey{ "closeWindow" };
|
||||
static constexpr std::string_view CloseTabKey{ "closeTab" };
|
||||
static constexpr std::string_view ClosePaneKey{ "closePane" };
|
||||
static constexpr std::string_view SwitchtoTabKey{ "switchToTab" };
|
||||
static constexpr std::string_view NextTabKey{ "nextTab" };
|
||||
static constexpr std::string_view PrevTabKey{ "prevTab" };
|
||||
static constexpr std::string_view AdjustFontSizeKey{ "adjustFontSize" };
|
||||
static constexpr std::string_view ResetFontSizeKey{ "resetFontSize" };
|
||||
static constexpr std::string_view ScrollupKey{ "scrollUp" };
|
||||
static constexpr std::string_view ScrolldownKey{ "scrollDown" };
|
||||
static constexpr std::string_view ScrolluppageKey{ "scrollUpPage" };
|
||||
static constexpr std::string_view ScrolldownpageKey{ "scrollDownPage" };
|
||||
static constexpr std::string_view SwitchToTabKey{ "switchToTab" };
|
||||
static constexpr std::string_view OpenSettingsKey{ "openSettings" }; // TODO GH#2557: Add args for OpenSettings
|
||||
static constexpr std::string_view SplitPaneKey{ "splitPane" };
|
||||
static constexpr std::string_view ResizePaneKey{ "resizePane" };
|
||||
static constexpr std::string_view MoveFocusKey{ "moveFocus" };
|
||||
static constexpr std::string_view FindKey{ "find" };
|
||||
static constexpr std::string_view ToggleFullscreenKey{ "toggleFullscreen" };
|
||||
|
||||
namespace winrt::TerminalApp::implementation
|
||||
{
|
||||
// Specifically use a map here over an unordered_map. We want to be able to
|
||||
// iterate over these entries in-order when we're serializing the keybindings.
|
||||
// HERE BE DRAGONS:
|
||||
// These are string_views that are being used as keys. These string_views are
|
||||
// just pointers to other strings. This could be dangerous, if the map outlived
|
||||
// the actual strings being pointed to. However, since both these strings and
|
||||
// the map are all const for the lifetime of the app, we have nothing to worry
|
||||
// about here.
|
||||
const std::map<std::string_view, ShortcutAction, std::less<>> ActionAndArgs::ActionNamesMap{
|
||||
{ CopyTextKey, ShortcutAction::CopyText },
|
||||
{ PasteTextKey, ShortcutAction::PasteText },
|
||||
{ OpenNewTabDropdownKey, ShortcutAction::OpenNewTabDropdown },
|
||||
{ DuplicateTabKey, ShortcutAction::DuplicateTab },
|
||||
{ NewTabKey, ShortcutAction::NewTab },
|
||||
{ NewWindowKey, ShortcutAction::NewWindow },
|
||||
{ CloseWindowKey, ShortcutAction::CloseWindow },
|
||||
{ CloseTabKey, ShortcutAction::CloseTab },
|
||||
{ ClosePaneKey, ShortcutAction::ClosePane },
|
||||
{ NextTabKey, ShortcutAction::NextTab },
|
||||
{ PrevTabKey, ShortcutAction::PrevTab },
|
||||
{ AdjustFontSizeKey, ShortcutAction::AdjustFontSize },
|
||||
{ ResetFontSizeKey, ShortcutAction::ResetFontSize },
|
||||
{ ScrollupKey, ShortcutAction::ScrollUp },
|
||||
{ ScrolldownKey, ShortcutAction::ScrollDown },
|
||||
{ ScrolluppageKey, ShortcutAction::ScrollUpPage },
|
||||
{ ScrolldownpageKey, ShortcutAction::ScrollDownPage },
|
||||
{ SwitchToTabKey, ShortcutAction::SwitchToTab },
|
||||
{ ResizePaneKey, ShortcutAction::ResizePane },
|
||||
{ MoveFocusKey, ShortcutAction::MoveFocus },
|
||||
{ OpenSettingsKey, ShortcutAction::OpenSettings },
|
||||
{ ToggleFullscreenKey, ShortcutAction::ToggleFullscreen },
|
||||
{ SplitPaneKey, ShortcutAction::SplitPane },
|
||||
{ UnboundKey, ShortcutAction::Invalid },
|
||||
{ FindKey, ShortcutAction::Find }
|
||||
};
|
||||
|
||||
using ParseResult = std::tuple<IActionArgs, std::vector<::TerminalApp::SettingsLoadWarnings>>;
|
||||
using ParseActionFunction = std::function<ParseResult(const Json::Value&)>;
|
||||
|
||||
// This is a map of ShortcutAction->function<IActionArgs(Json::Value)>. It holds
|
||||
// a set of deserializer functions that can be used to deserialize a IActionArgs
|
||||
// from json. Each type of IActionArgs that can accept arbitrary args should be
|
||||
// placed into this map, with the corresponding deserializer function as the
|
||||
// value.
|
||||
static const std::map<ShortcutAction, ParseActionFunction, std::less<>> argParsers{
|
||||
{ ShortcutAction::CopyText, winrt::TerminalApp::implementation::CopyTextArgs::FromJson },
|
||||
|
||||
{ ShortcutAction::NewTab, winrt::TerminalApp::implementation::NewTabArgs::FromJson },
|
||||
|
||||
{ ShortcutAction::SwitchToTab, winrt::TerminalApp::implementation::SwitchToTabArgs::FromJson },
|
||||
|
||||
{ ShortcutAction::ResizePane, winrt::TerminalApp::implementation::ResizePaneArgs::FromJson },
|
||||
|
||||
{ ShortcutAction::MoveFocus, winrt::TerminalApp::implementation::MoveFocusArgs::FromJson },
|
||||
|
||||
{ ShortcutAction::AdjustFontSize, winrt::TerminalApp::implementation::AdjustFontSizeArgs::FromJson },
|
||||
|
||||
{ ShortcutAction::SplitPane, winrt::TerminalApp::implementation::SplitPaneArgs::FromJson },
|
||||
|
||||
{ ShortcutAction::OpenSettings, winrt::TerminalApp::implementation::OpenSettingsArgs::FromJson },
|
||||
|
||||
{ ShortcutAction::Invalid, nullptr },
|
||||
};
|
||||
|
||||
// Function Description:
|
||||
// - Attempts to match a string to a ShortcutAction. If there's no match, then
|
||||
// returns ShortcutAction::Invalid
|
||||
// Arguments:
|
||||
// - actionString: the string to match to a ShortcutAction
|
||||
// Return Value:
|
||||
// - The ShortcutAction corresponding to the given string, if a match exists.
|
||||
static ShortcutAction GetActionFromString(const std::string_view actionString)
|
||||
{
|
||||
// Try matching the command to one we have. If we can't find the
|
||||
// action name in our list of names, let's just unbind that key.
|
||||
const auto found = ActionAndArgs::ActionNamesMap.find(actionString);
|
||||
return found != ActionAndArgs::ActionNamesMap.end() ? found->second : ShortcutAction::Invalid;
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Deserialize an ActionAndArgs from the provided json object or string `json`.
|
||||
// * If json is a string, we'll attempt to treat it as an action name,
|
||||
// without arguments.
|
||||
// * If json is an object, we'll attempt to retrieve the action name from
|
||||
// its "action" property, and we'll use that name to fine a deserializer
|
||||
// to precess the rest of the arguments in the json object.
|
||||
// - If the action name is null or "unbound", or we don't understand the
|
||||
// action name, or we failed to parse the arguments to this action, we'll
|
||||
// return null. This should indicate to the caller that the action should
|
||||
// be unbound.
|
||||
// - If there were any warnings while parsing arguments for the action,
|
||||
// they'll be appended to the warnings parameter.
|
||||
// Arguments:
|
||||
// - json: The Json::Value to attempt to parse as an ActionAndArgs
|
||||
// - warnings: If there were any warnings during parsing, they'll be
|
||||
// appended to this vector.
|
||||
// Return Value:
|
||||
// - a deserialized ActionAndArgs corresponding to the values in json, or
|
||||
// null if we failed to deserialize an action.
|
||||
winrt::com_ptr<ActionAndArgs> ActionAndArgs::FromJson(const Json::Value& json,
|
||||
std::vector<::TerminalApp::SettingsLoadWarnings>& warnings)
|
||||
{
|
||||
// Invalid is our placeholder that the action was not parsed.
|
||||
ShortcutAction action = ShortcutAction::Invalid;
|
||||
|
||||
// Actions can be serialized in two styles:
|
||||
// "action": "switchToTab0",
|
||||
// "action": { "action": "switchToTab", "index": 0 },
|
||||
// NOTE: For keybindings, the "action" param is actually "command"
|
||||
|
||||
// 1. In the first case, the json is a string, that's the
|
||||
// action name. There are no provided args, so we'll pass
|
||||
// Json::Value::null to the parse function.
|
||||
// 2. In the second case, the json is an object. We'll use the
|
||||
// "action" in that object as the action name. We'll then pass
|
||||
// the json object to the arg parser, for further parsing.
|
||||
|
||||
auto argsVal = Json::Value::null;
|
||||
|
||||
// Only try to parse the action if it's actually a string value.
|
||||
// `null` will not pass this check.
|
||||
if (json.isString())
|
||||
{
|
||||
auto commandString = json.asString();
|
||||
action = GetActionFromString(commandString);
|
||||
}
|
||||
else if (json.isObject())
|
||||
{
|
||||
const auto actionVal = json[JsonKey(ActionKey)];
|
||||
if (actionVal.isString())
|
||||
{
|
||||
auto actionString = actionVal.asString();
|
||||
action = GetActionFromString(actionString);
|
||||
argsVal = json;
|
||||
}
|
||||
}
|
||||
|
||||
// Some keybindings can accept other arbitrary arguments. If it
|
||||
// does, we'll try to deserialize any "args" that were provided with
|
||||
// the binding.
|
||||
IActionArgs args{ nullptr };
|
||||
std::vector<::TerminalApp::SettingsLoadWarnings> parseWarnings;
|
||||
const auto deserializersIter = argParsers.find(action);
|
||||
if (deserializersIter != argParsers.end())
|
||||
{
|
||||
auto pfn = deserializersIter->second;
|
||||
if (pfn)
|
||||
{
|
||||
std::tie(args, parseWarnings) = pfn(argsVal);
|
||||
}
|
||||
warnings.insert(warnings.end(), parseWarnings.begin(), parseWarnings.end());
|
||||
|
||||
// if an arg parser was registered, but failed, bail
|
||||
if (pfn && args == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
if (action != ShortcutAction::Invalid)
|
||||
{
|
||||
auto actionAndArgs = winrt::make_self<ActionAndArgs>();
|
||||
actionAndArgs->Action(action);
|
||||
actionAndArgs->Args(args);
|
||||
|
||||
return actionAndArgs;
|
||||
}
|
||||
else
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
#pragma once
|
||||
#include "ActionAndArgs.g.h"
|
||||
#include "TerminalWarnings.h"
|
||||
#include "..\inc\cppwinrt_utils.h"
|
||||
|
||||
namespace winrt::TerminalApp::implementation
|
||||
{
|
||||
struct ActionAndArgs : public ActionAndArgsT<ActionAndArgs>
|
||||
{
|
||||
static const std::map<std::string_view, ShortcutAction, std::less<>> ActionNamesMap;
|
||||
static winrt::com_ptr<ActionAndArgs> FromJson(const Json::Value& json,
|
||||
std::vector<::TerminalApp::SettingsLoadWarnings>& warnings);
|
||||
|
||||
ActionAndArgs() = default;
|
||||
GETSET_PROPERTY(TerminalApp::ShortcutAction, Action, TerminalApp::ShortcutAction::Invalid);
|
||||
GETSET_PROPERTY(IActionArgs, Args, nullptr);
|
||||
|
|
|
@ -14,3 +14,4 @@
|
|||
#include "MoveFocusArgs.g.cpp"
|
||||
#include "AdjustFontSizeArgs.g.cpp"
|
||||
#include "SplitPaneArgs.g.cpp"
|
||||
#include "OpenSettingsArgs.g.cpp"
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
#include "MoveFocusArgs.g.h"
|
||||
#include "AdjustFontSizeArgs.g.h"
|
||||
#include "SplitPaneArgs.g.h"
|
||||
#include "OpenSettingsArgs.g.h"
|
||||
|
||||
#include "../../cascadia/inc/cppwinrt_utils.h"
|
||||
#include "Utils.h"
|
||||
|
@ -381,6 +382,65 @@ namespace winrt::TerminalApp::implementation
|
|||
return { *args, {} };
|
||||
}
|
||||
};
|
||||
|
||||
// Possible SettingsTarget values
|
||||
// TODO:GH#2550/#3475 - move these to a centralized deserializing place
|
||||
static constexpr std::string_view SettingsFileString{ "settingsFile" };
|
||||
static constexpr std::string_view DefaultsFileString{ "defaultsFile" };
|
||||
static constexpr std::string_view AllFilesString{ "allFiles" };
|
||||
|
||||
// Function Description:
|
||||
// - Helper function for parsing a SettingsTarget from a string
|
||||
// Arguments:
|
||||
// - targetString: the string to attempt to parse
|
||||
// Return Value:
|
||||
// - The encoded SettingsTarget value, or SettingsTarget::SettingsFile if it was an invalid string
|
||||
static TerminalApp::SettingsTarget ParseSettingsTarget(const std::string& targetString)
|
||||
{
|
||||
if (targetString == SettingsFileString)
|
||||
{
|
||||
return TerminalApp::SettingsTarget::SettingsFile;
|
||||
}
|
||||
else if (targetString == DefaultsFileString)
|
||||
{
|
||||
return TerminalApp::SettingsTarget::DefaultsFile;
|
||||
}
|
||||
else if (targetString == AllFilesString)
|
||||
{
|
||||
return TerminalApp::SettingsTarget::AllFiles;
|
||||
}
|
||||
// default behavior for invalid data
|
||||
return TerminalApp::SettingsTarget::SettingsFile;
|
||||
};
|
||||
|
||||
struct OpenSettingsArgs : public OpenSettingsArgsT<OpenSettingsArgs>
|
||||
{
|
||||
OpenSettingsArgs() = default;
|
||||
GETSET_PROPERTY(TerminalApp::SettingsTarget, Target, TerminalApp::SettingsTarget::SettingsFile);
|
||||
|
||||
static constexpr std::string_view TargetKey{ "target" };
|
||||
|
||||
public:
|
||||
bool Equals(const IActionArgs& other)
|
||||
{
|
||||
auto otherAsUs = other.try_as<OpenSettingsArgs>();
|
||||
if (otherAsUs)
|
||||
{
|
||||
return otherAsUs->_Target == _Target;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
static FromJsonResult FromJson(const Json::Value& json)
|
||||
{
|
||||
// LOAD BEARING: Not using make_self here _will_ break you in the future!
|
||||
auto args = winrt::make_self<OpenSettingsArgs>();
|
||||
if (auto targetString{ json[JsonKey(TargetKey)] })
|
||||
{
|
||||
args->_Target = ParseSettingsTarget(targetString.asString());
|
||||
}
|
||||
return { *args, {} };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
namespace winrt::TerminalApp::factory_implementation
|
||||
|
|
|
@ -37,6 +37,13 @@ namespace TerminalApp
|
|||
Duplicate = 1
|
||||
};
|
||||
|
||||
enum SettingsTarget
|
||||
{
|
||||
SettingsFile = 0,
|
||||
DefaultsFile,
|
||||
AllFiles
|
||||
};
|
||||
|
||||
[default_interface] runtimeclass NewTerminalArgs {
|
||||
NewTerminalArgs();
|
||||
String Commandline;
|
||||
|
@ -90,4 +97,9 @@ namespace TerminalApp
|
|||
NewTerminalArgs TerminalArgs { get; };
|
||||
SplitType SplitMode { get; };
|
||||
};
|
||||
|
||||
[default_interface] runtimeclass OpenSettingsArgs : IActionArgs
|
||||
{
|
||||
SettingsTarget Target { get; };
|
||||
};
|
||||
}
|
||||
|
|
|
@ -120,9 +120,11 @@ namespace winrt::TerminalApp::implementation
|
|||
void TerminalPage::_HandleOpenSettings(const IInspectable& /*sender*/,
|
||||
const TerminalApp::ActionEventArgs& args)
|
||||
{
|
||||
// TODO:GH#2557 Add an optional arg for opening the defaults here
|
||||
_LaunchSettings(false);
|
||||
args.Handled(true);
|
||||
if (const auto& realArgs = args.ActionArgs().try_as<TerminalApp::OpenSettingsArgs>())
|
||||
{
|
||||
_LaunchSettings(realArgs.Target());
|
||||
args.Handled(true);
|
||||
}
|
||||
}
|
||||
|
||||
void TerminalPage::_HandlePasteText(const IInspectable& /*sender*/,
|
||||
|
|
|
@ -21,97 +21,6 @@ static constexpr std::string_view KeysKey{ "keys" };
|
|||
static constexpr std::string_view CommandKey{ "command" };
|
||||
static constexpr std::string_view ActionKey{ "action" };
|
||||
|
||||
// This key is reserved to remove a keybinding, instead of mapping it to an action.
|
||||
static constexpr std::string_view UnboundKey{ "unbound" };
|
||||
|
||||
static constexpr std::string_view CopyTextKey{ "copy" };
|
||||
static constexpr std::string_view PasteTextKey{ "paste" };
|
||||
static constexpr std::string_view OpenNewTabDropdownKey{ "openNewTabDropdown" };
|
||||
static constexpr std::string_view DuplicateTabKey{ "duplicateTab" };
|
||||
static constexpr std::string_view NewTabKey{ "newTab" };
|
||||
static constexpr std::string_view NewWindowKey{ "newWindow" };
|
||||
static constexpr std::string_view CloseWindowKey{ "closeWindow" };
|
||||
static constexpr std::string_view CloseTabKey{ "closeTab" };
|
||||
static constexpr std::string_view ClosePaneKey{ "closePane" };
|
||||
static constexpr std::string_view SwitchtoTabKey{ "switchToTab" };
|
||||
static constexpr std::string_view NextTabKey{ "nextTab" };
|
||||
static constexpr std::string_view PrevTabKey{ "prevTab" };
|
||||
static constexpr std::string_view AdjustFontSizeKey{ "adjustFontSize" };
|
||||
static constexpr std::string_view ResetFontSizeKey{ "resetFontSize" };
|
||||
static constexpr std::string_view ScrollupKey{ "scrollUp" };
|
||||
static constexpr std::string_view ScrolldownKey{ "scrollDown" };
|
||||
static constexpr std::string_view ScrolluppageKey{ "scrollUpPage" };
|
||||
static constexpr std::string_view ScrolldownpageKey{ "scrollDownPage" };
|
||||
static constexpr std::string_view SwitchToTabKey{ "switchToTab" };
|
||||
static constexpr std::string_view OpenSettingsKey{ "openSettings" }; // TODO GH#2557: Add args for OpenSettings
|
||||
static constexpr std::string_view SplitPaneKey{ "splitPane" };
|
||||
static constexpr std::string_view ResizePaneKey{ "resizePane" };
|
||||
static constexpr std::string_view MoveFocusKey{ "moveFocus" };
|
||||
static constexpr std::string_view FindKey{ "find" };
|
||||
static constexpr std::string_view ToggleFullscreenKey{ "toggleFullscreen" };
|
||||
|
||||
// Specifically use a map here over an unordered_map. We want to be able to
|
||||
// iterate over these entries in-order when we're serializing the keybindings.
|
||||
// HERE BE DRAGONS:
|
||||
// These are string_views that are being used as keys. These string_views are
|
||||
// just pointers to other strings. This could be dangerous, if the map outlived
|
||||
// the actual strings being pointed to. However, since both these strings and
|
||||
// the map are all const for the lifetime of the app, we have nothing to worry
|
||||
// about here.
|
||||
static const std::map<std::string_view, ShortcutAction, std::less<>> commandNames{
|
||||
{ CopyTextKey, ShortcutAction::CopyText },
|
||||
{ PasteTextKey, ShortcutAction::PasteText },
|
||||
{ OpenNewTabDropdownKey, ShortcutAction::OpenNewTabDropdown },
|
||||
{ DuplicateTabKey, ShortcutAction::DuplicateTab },
|
||||
{ NewTabKey, ShortcutAction::NewTab },
|
||||
{ NewWindowKey, ShortcutAction::NewWindow },
|
||||
{ CloseWindowKey, ShortcutAction::CloseWindow },
|
||||
{ CloseTabKey, ShortcutAction::CloseTab },
|
||||
{ ClosePaneKey, ShortcutAction::ClosePane },
|
||||
{ NextTabKey, ShortcutAction::NextTab },
|
||||
{ PrevTabKey, ShortcutAction::PrevTab },
|
||||
{ AdjustFontSizeKey, ShortcutAction::AdjustFontSize },
|
||||
{ ResetFontSizeKey, ShortcutAction::ResetFontSize },
|
||||
{ ScrollupKey, ShortcutAction::ScrollUp },
|
||||
{ ScrolldownKey, ShortcutAction::ScrollDown },
|
||||
{ ScrolluppageKey, ShortcutAction::ScrollUpPage },
|
||||
{ ScrolldownpageKey, ShortcutAction::ScrollDownPage },
|
||||
{ SwitchToTabKey, ShortcutAction::SwitchToTab },
|
||||
{ ResizePaneKey, ShortcutAction::ResizePane },
|
||||
{ MoveFocusKey, ShortcutAction::MoveFocus },
|
||||
{ OpenSettingsKey, ShortcutAction::OpenSettings },
|
||||
{ ToggleFullscreenKey, ShortcutAction::ToggleFullscreen },
|
||||
{ SplitPaneKey, ShortcutAction::SplitPane },
|
||||
{ UnboundKey, ShortcutAction::Invalid },
|
||||
{ FindKey, ShortcutAction::Find },
|
||||
};
|
||||
|
||||
using ParseResult = std::tuple<IActionArgs, std::vector<TerminalApp::SettingsLoadWarnings>>;
|
||||
using ParseActionFunction = std::function<ParseResult(const Json::Value&)>;
|
||||
|
||||
// This is a map of ShortcutAction->function<IActionArgs(Json::Value)>. It holds
|
||||
// a set of deserializer functions that can be used to deserialize a IActionArgs
|
||||
// from json. Each type of IActionArgs that can accept arbitrary args should be
|
||||
// placed into this map, with the corresponding deserializer function as the
|
||||
// value.
|
||||
static const std::map<ShortcutAction, ParseActionFunction, std::less<>> argParsers{
|
||||
{ ShortcutAction::CopyText, winrt::TerminalApp::implementation::CopyTextArgs::FromJson },
|
||||
|
||||
{ ShortcutAction::NewTab, winrt::TerminalApp::implementation::NewTabArgs::FromJson },
|
||||
|
||||
{ ShortcutAction::SwitchToTab, winrt::TerminalApp::implementation::SwitchToTabArgs::FromJson },
|
||||
|
||||
{ ShortcutAction::ResizePane, winrt::TerminalApp::implementation::ResizePaneArgs::FromJson },
|
||||
|
||||
{ ShortcutAction::MoveFocus, winrt::TerminalApp::implementation::MoveFocusArgs::FromJson },
|
||||
|
||||
{ ShortcutAction::AdjustFontSize, winrt::TerminalApp::implementation::AdjustFontSizeArgs::FromJson },
|
||||
|
||||
{ ShortcutAction::SplitPane, winrt::TerminalApp::implementation::SplitPaneArgs::FromJson },
|
||||
|
||||
{ ShortcutAction::Invalid, nullptr },
|
||||
};
|
||||
|
||||
// Function Description:
|
||||
// - Small helper to create a json value serialization of a single
|
||||
// KeyBinding->Action mapping.
|
||||
|
@ -155,7 +64,7 @@ Json::Value winrt::TerminalApp::implementation::AppKeyBindings::ToJson()
|
|||
|
||||
// Iterate over all the possible actions in the names list, and see if
|
||||
// it has a binding.
|
||||
for (auto& actionName : commandNames)
|
||||
for (auto& actionName : ActionAndArgs::ActionNamesMap)
|
||||
{
|
||||
const auto searchedForName = actionName.first;
|
||||
const auto searchedForAction = actionName.second;
|
||||
|
@ -172,34 +81,19 @@ Json::Value winrt::TerminalApp::implementation::AppKeyBindings::ToJson()
|
|||
return bindingsArray;
|
||||
}
|
||||
|
||||
// Function Description:
|
||||
// - Attempts to match a string to a ShortcutAction. If there's no match, then
|
||||
// returns ShortcutAction::Invalid
|
||||
// Arguments:
|
||||
// - actionString: the string to match to a ShortcutAction
|
||||
// Return Value:
|
||||
// - The ShortcutAction corresponding to the given string, if a match exists.
|
||||
static ShortcutAction GetActionFromString(const std::string_view actionString)
|
||||
{
|
||||
// Try matching the command to one we have. If we can't find the
|
||||
// action name in our list of names, let's just unbind that key.
|
||||
const auto found = commandNames.find(actionString);
|
||||
return found != commandNames.end() ? found->second : ShortcutAction::Invalid;
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Deserialize an AppKeyBindings from the key mappings that are in the array
|
||||
// `json`. The json array should contain an array of objects with both a
|
||||
// `command` string and a `keys` array, where `command` is one of the names
|
||||
// listed in `commandNames`, and `keys` is an array of keypresses. Currently,
|
||||
// the array should contain a single string, which can be deserialized into a
|
||||
// KeyChord.
|
||||
// listed in `ActionAndArgs::ActionNamesMap`, and `keys` is an array of
|
||||
// keypresses. Currently, the array should contain a single string, which can
|
||||
// be deserialized into a KeyChord.
|
||||
// - Applies the deserialized keybindings to the provided `bindings` object. If
|
||||
// a key chord in `json` is already bound to an action, that chord will be
|
||||
// overwritten with the new action. If a chord is bound to `null` or
|
||||
// `"unbound"`, then we'll clear the keybinding from the existing keybindings.
|
||||
// Arguments:
|
||||
// - json: and array of JsonObject's to deserialize into our _keyShortcuts mapping.
|
||||
// - json: an array of Json::Value's to deserialize into our _keyShortcuts mapping.
|
||||
std::vector<::TerminalApp::SettingsLoadWarnings> winrt::TerminalApp::implementation::AppKeyBindings::LayerJson(const Json::Value& json)
|
||||
{
|
||||
// It's possible that the user provided keybindings have some warnings in
|
||||
|
@ -236,61 +130,10 @@ std::vector<::TerminalApp::SettingsLoadWarnings> winrt::TerminalApp::implementat
|
|||
continue;
|
||||
}
|
||||
const auto keyChordString = keys.isString() ? winrt::to_hstring(keys.asString()) : winrt::to_hstring(keys[0].asString());
|
||||
// Invalid is our placeholder that the action was not parsed.
|
||||
ShortcutAction action = ShortcutAction::Invalid;
|
||||
|
||||
// Keybindings can be serialized in two styles:
|
||||
// { "command": "switchToTab0", "keys": ["ctrl+1"] },
|
||||
// { "command": { "action": "switchToTab", "index": 0 }, "keys": ["ctrl+alt+1"] },
|
||||
|
||||
// 1. In the first case, the "command" is a string, that's the
|
||||
// action name. There are no provided args, so we'll pass
|
||||
// Json::Value::null to the parse function.
|
||||
// 2. In the second case, the "command" is an object. We'll use the
|
||||
// "action" in that object as the action name. We'll then pass
|
||||
// the "command" object to the arg parser, for further parsing.
|
||||
|
||||
auto argsVal = Json::Value::null;
|
||||
|
||||
// Only try to parse the action if it's actually a string value.
|
||||
// `null` will not pass this check.
|
||||
if (commandVal.isString())
|
||||
{
|
||||
auto commandString = commandVal.asString();
|
||||
action = GetActionFromString(commandString);
|
||||
}
|
||||
else if (commandVal.isObject())
|
||||
{
|
||||
const auto actionVal = commandVal[JsonKey(ActionKey)];
|
||||
if (actionVal.isString())
|
||||
{
|
||||
auto actionString = actionVal.asString();
|
||||
action = GetActionFromString(actionString);
|
||||
argsVal = commandVal;
|
||||
}
|
||||
}
|
||||
|
||||
// Some keybindings can accept other arbitrary arguments. If it
|
||||
// does, we'll try to deserialize any "args" that were provided with
|
||||
// the binding.
|
||||
IActionArgs args{ nullptr };
|
||||
std::vector<::TerminalApp::SettingsLoadWarnings> parseWarnings;
|
||||
const auto deserializersIter = argParsers.find(action);
|
||||
if (deserializersIter != argParsers.end())
|
||||
{
|
||||
auto pfn = deserializersIter->second;
|
||||
if (pfn)
|
||||
{
|
||||
std::tie(args, parseWarnings) = pfn(argsVal);
|
||||
}
|
||||
warnings.insert(warnings.end(), parseWarnings.begin(), parseWarnings.end());
|
||||
|
||||
// if an arg parser was registered, but failed, bail
|
||||
if (pfn && args == nullptr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// If the action was null, "unbound", or something we didn't
|
||||
// understand, this will return nullptr.
|
||||
auto actionAndArgs = ActionAndArgs::FromJson(commandVal, warnings);
|
||||
|
||||
// Try parsing the chord
|
||||
try
|
||||
|
@ -301,11 +144,8 @@ std::vector<::TerminalApp::SettingsLoadWarnings> winrt::TerminalApp::implementat
|
|||
// or the action was `null` or `"unbound"`, just clear out the
|
||||
// keybinding. Otherwise, set the keybinding to the action we
|
||||
// found.
|
||||
if (action != ShortcutAction::Invalid)
|
||||
if (actionAndArgs)
|
||||
{
|
||||
auto actionAndArgs = winrt::make_self<ActionAndArgs>();
|
||||
actionAndArgs->Action(action);
|
||||
actionAndArgs->Args(args);
|
||||
SetKeyBinding(*actionAndArgs, chord);
|
||||
}
|
||||
else
|
||||
|
|
|
@ -906,20 +906,21 @@ namespace winrt::TerminalApp::implementation
|
|||
|
||||
// Method Description:
|
||||
// - Implements the F7 handler (per GH#638)
|
||||
// - Implements the Alt handler (per GH#6421)
|
||||
// Return value:
|
||||
// - whether F7 was handled
|
||||
bool AppLogic::OnF7Pressed()
|
||||
// - whether the key was handled
|
||||
bool AppLogic::OnDirectKeyEvent(const uint32_t vkey, const bool down)
|
||||
{
|
||||
if (_root)
|
||||
{
|
||||
// Manually bubble the OnF7Pressed event up through the focus tree.
|
||||
// Manually bubble the OnDirectKeyEvent event up through the focus tree.
|
||||
auto xamlRoot{ _root->XamlRoot() };
|
||||
auto focusedObject{ Windows::UI::Xaml::Input::FocusManager::GetFocusedElement(xamlRoot) };
|
||||
do
|
||||
{
|
||||
if (auto f7Listener{ focusedObject.try_as<IF7Listener>() })
|
||||
if (auto keyListener{ focusedObject.try_as<IDirectKeyListener>() })
|
||||
{
|
||||
if (f7Listener.OnF7Pressed())
|
||||
if (keyListener.OnDirectKeyEvent(vkey, down))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ namespace winrt::TerminalApp::implementation
|
|||
|
||||
hstring Title();
|
||||
void TitlebarClicked();
|
||||
bool OnF7Pressed();
|
||||
bool OnDirectKeyEvent(const uint32_t vkey, const bool down);
|
||||
|
||||
void WindowCloseButtonClicked();
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import "../TerminalPage.idl";
|
||||
import "../ShortcutActionDispatch.idl";
|
||||
import "../IF7Listener.idl";
|
||||
import "../IDirectKeyListener.idl";
|
||||
|
||||
namespace TerminalApp
|
||||
{
|
||||
|
@ -14,7 +14,7 @@ namespace TerminalApp
|
|||
FullscreenMode,
|
||||
};
|
||||
|
||||
[default_interface] runtimeclass AppLogic : IF7Listener
|
||||
[default_interface] runtimeclass AppLogic : IDirectKeyListener
|
||||
{
|
||||
AppLogic();
|
||||
|
||||
|
|
|
@ -34,12 +34,13 @@ static constexpr std::string_view CopyFormattingKey{ "copyFormatting" };
|
|||
static constexpr std::string_view LaunchModeKey{ "launchMode" };
|
||||
static constexpr std::string_view ConfirmCloseAllKey{ "confirmCloseAllTabs" };
|
||||
static constexpr std::string_view SnapToGridOnResizeKey{ "snapToGridOnResize" };
|
||||
static constexpr std::string_view EnableStartupTaskKey{ "startOnUserLogin" };
|
||||
|
||||
static constexpr std::string_view DebugFeaturesKey{ "debugFeatures" };
|
||||
|
||||
static constexpr std::string_view ForceFullRepaintRenderingKey{ "experimental.rendering.forceFullRepaint" };
|
||||
static constexpr std::string_view SoftwareRenderingKey{ "experimental.rendering.software" };
|
||||
static constexpr std::string_view EnableStartupTaskKey{ "startOnUserLogin" };
|
||||
static constexpr std::string_view ForceVTInputKey{ "experimental.input.forceVT" };
|
||||
|
||||
// Launch mode values
|
||||
static constexpr std::wstring_view DefaultLaunchModeValue{ L"default" };
|
||||
|
@ -130,6 +131,7 @@ void GlobalAppSettings::ApplyToSettings(TerminalSettings& settings) const noexce
|
|||
settings.CopyOnSelect(_CopyOnSelect);
|
||||
settings.ForceFullRepaintRendering(_ForceFullRepaintRendering);
|
||||
settings.SoftwareRendering(_SoftwareRendering);
|
||||
settings.ForceVTInput(_ForceVTInput);
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
|
@ -220,6 +222,7 @@ void GlobalAppSettings::LayerJson(const Json::Value& json)
|
|||
JsonUtils::GetBool(json, ForceFullRepaintRenderingKey, _ForceFullRepaintRendering);
|
||||
|
||||
JsonUtils::GetBool(json, SoftwareRenderingKey, _SoftwareRendering);
|
||||
JsonUtils::GetBool(json, ForceVTInputKey, _ForceVTInput);
|
||||
|
||||
// GetBool will only override the current value if the key exists
|
||||
JsonUtils::GetBool(json, DebugFeaturesKey, _DebugFeaturesEnabled);
|
||||
|
|
|
@ -77,6 +77,7 @@ public:
|
|||
GETSET_PROPERTY(bool, SnapToGridOnResize, true);
|
||||
GETSET_PROPERTY(bool, ForceFullRepaintRendering, false);
|
||||
GETSET_PROPERTY(bool, SoftwareRendering, false);
|
||||
GETSET_PROPERTY(bool, ForceVTInput, false);
|
||||
GETSET_PROPERTY(bool, DebugFeaturesEnabled); // default value set in constructor
|
||||
|
||||
GETSET_PROPERTY(bool, StartOnUserLogin, false);
|
||||
|
|
|
@ -7,10 +7,8 @@ namespace TerminalApp
|
|||
// Instead, we just pin the uuid and include it in both TermControl and App
|
||||
// If you update this one, please update the one in TerminalControl\TermControl.idl
|
||||
// If you change this interface, please update the guid.
|
||||
// If you press F7 and get a runtime error, go make sure both copies are the same.
|
||||
[uuid("339e1a87-5315-4da6-96f0-565549b6472b")]
|
||||
interface IF7Listener
|
||||
{
|
||||
Boolean OnF7Pressed();
|
||||
// If you press F7 or Alt and get a runtime error, go make sure both copies are the same.
|
||||
[uuid("339e1a87-5315-4da6-96f0-565549b6472b")] interface IDirectKeyListener {
|
||||
Boolean OnDirectKeyEvent(UInt32 vkey, Boolean down);
|
||||
}
|
||||
}
|
471
src/cascadia/TerminalApp/JsonUtilsNew.h
Normal file
|
@ -0,0 +1,471 @@
|
|||
/*++
|
||||
Copyright (c) Microsoft Corporation
|
||||
Licensed under the MIT license.
|
||||
|
||||
Module Name:
|
||||
- JsonUtils.h
|
||||
|
||||
Abstract:
|
||||
- Helpers for the TerminalApp project
|
||||
Author(s):
|
||||
- Mike Griese - August 2019
|
||||
- Dustin Howett - January 2020
|
||||
--*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <json.h>
|
||||
|
||||
#include "../types/inc/utils.hpp"
|
||||
|
||||
namespace winrt
|
||||
{
|
||||
// If we don't use winrt, nobody will include the ConversionTrait for winrt::guid.
|
||||
// If nobody includes it, this forward declaration will suffice.
|
||||
struct guid;
|
||||
}
|
||||
|
||||
namespace TerminalApp::JsonUtils
|
||||
{
|
||||
namespace Detail
|
||||
{
|
||||
// Function Description:
|
||||
// - Returns a string_view to a Json::Value's internal string storage,
|
||||
// hopefully without copying it.
|
||||
__declspec(noinline) inline const std::string_view GetStringView(const Json::Value& json)
|
||||
{
|
||||
const char* begin{ nullptr };
|
||||
const char* end{ nullptr };
|
||||
json.getString(&begin, &end);
|
||||
const std::string_view zeroCopyString{ begin, gsl::narrow_cast<size_t>(end - begin) };
|
||||
return zeroCopyString;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
struct DeduceOptional
|
||||
{
|
||||
using Type = typename std::decay<T>::type;
|
||||
};
|
||||
|
||||
template<typename TOpt>
|
||||
struct DeduceOptional<std::optional<TOpt>>
|
||||
{
|
||||
using Type = typename std::decay<TOpt>::type;
|
||||
};
|
||||
}
|
||||
|
||||
// These exceptions cannot use localized messages, as we do not have
|
||||
// guaranteed access to the resource loader.
|
||||
class TypeMismatchException : public std::runtime_error
|
||||
{
|
||||
public:
|
||||
TypeMismatchException() :
|
||||
runtime_error("unexpected data type") {}
|
||||
};
|
||||
|
||||
class KeyedException : public std::runtime_error
|
||||
{
|
||||
public:
|
||||
KeyedException(const std::string_view key, std::exception_ptr exception) :
|
||||
runtime_error(fmt::format("error parsing \"{0}\"", key).c_str()),
|
||||
_key{ key },
|
||||
_innerException{ std::move(exception) } {}
|
||||
|
||||
std::string GetKey() const
|
||||
{
|
||||
return _key;
|
||||
}
|
||||
|
||||
[[noreturn]] void RethrowInner() const
|
||||
{
|
||||
std::rethrow_exception(_innerException);
|
||||
}
|
||||
|
||||
private:
|
||||
std::string _key;
|
||||
std::exception_ptr _innerException;
|
||||
};
|
||||
|
||||
class UnexpectedValueException : public std::runtime_error
|
||||
{
|
||||
public:
|
||||
UnexpectedValueException(const std::string_view value) :
|
||||
runtime_error(fmt::format("unexpected value \"{0}\"", value).c_str()),
|
||||
_value{ value } {}
|
||||
|
||||
std::string GetValue() const
|
||||
{
|
||||
return _value;
|
||||
}
|
||||
|
||||
private:
|
||||
std::string _value;
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
struct ConversionTrait
|
||||
{
|
||||
// FromJson, CanConvert are not defined so as to cause a compile error (which forces a specialization)
|
||||
};
|
||||
|
||||
template<>
|
||||
struct ConversionTrait<std::string>
|
||||
{
|
||||
std::string FromJson(const Json::Value& json)
|
||||
{
|
||||
return json.asString();
|
||||
}
|
||||
|
||||
bool CanConvert(const Json::Value& json)
|
||||
{
|
||||
return json.isString();
|
||||
}
|
||||
};
|
||||
|
||||
template<>
|
||||
struct ConversionTrait<std::wstring>
|
||||
{
|
||||
std::wstring FromJson(const Json::Value& json)
|
||||
{
|
||||
return til::u8u16(Detail::GetStringView(json));
|
||||
}
|
||||
|
||||
bool CanConvert(const Json::Value& json)
|
||||
{
|
||||
return json.isString();
|
||||
}
|
||||
};
|
||||
|
||||
template<>
|
||||
struct ConversionTrait<bool>
|
||||
{
|
||||
bool FromJson(const Json::Value& json)
|
||||
{
|
||||
return json.asBool();
|
||||
}
|
||||
|
||||
bool CanConvert(const Json::Value& json)
|
||||
{
|
||||
return json.isBool();
|
||||
}
|
||||
};
|
||||
|
||||
template<>
|
||||
struct ConversionTrait<int>
|
||||
{
|
||||
int FromJson(const Json::Value& json)
|
||||
{
|
||||
return json.asInt();
|
||||
}
|
||||
|
||||
bool CanConvert(const Json::Value& json)
|
||||
{
|
||||
return json.isInt();
|
||||
}
|
||||
};
|
||||
|
||||
template<>
|
||||
struct ConversionTrait<unsigned int>
|
||||
{
|
||||
unsigned int FromJson(const Json::Value& json)
|
||||
{
|
||||
return json.asUInt();
|
||||
}
|
||||
|
||||
bool CanConvert(const Json::Value& json)
|
||||
{
|
||||
return json.isUInt();
|
||||
}
|
||||
};
|
||||
|
||||
template<>
|
||||
struct ConversionTrait<float>
|
||||
{
|
||||
float FromJson(const Json::Value& json)
|
||||
{
|
||||
return json.asFloat();
|
||||
}
|
||||
|
||||
bool CanConvert(const Json::Value& json)
|
||||
{
|
||||
return json.isNumeric();
|
||||
}
|
||||
};
|
||||
|
||||
template<>
|
||||
struct ConversionTrait<double>
|
||||
{
|
||||
double FromJson(const Json::Value& json)
|
||||
{
|
||||
return json.asDouble();
|
||||
}
|
||||
|
||||
bool CanConvert(const Json::Value& json)
|
||||
{
|
||||
return json.isNumeric();
|
||||
}
|
||||
};
|
||||
|
||||
template<>
|
||||
struct ConversionTrait<GUID>
|
||||
{
|
||||
GUID FromJson(const Json::Value& json)
|
||||
{
|
||||
return ::Microsoft::Console::Utils::GuidFromString(til::u8u16(Detail::GetStringView(json)));
|
||||
}
|
||||
|
||||
bool CanConvert(const Json::Value& json)
|
||||
{
|
||||
if (!json.isString())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto string{ Detail::GetStringView(json) };
|
||||
return string.length() == 38 && string.front() == '{' && string.back() == '}';
|
||||
}
|
||||
};
|
||||
|
||||
// (GUID and winrt::guid are mutually convertible!)
|
||||
template<>
|
||||
struct ConversionTrait<winrt::guid> : public ConversionTrait<GUID>
|
||||
{
|
||||
};
|
||||
|
||||
template<>
|
||||
struct ConversionTrait<til::color>
|
||||
{
|
||||
til::color FromJson(const Json::Value& json)
|
||||
{
|
||||
return ::Microsoft::Console::Utils::ColorFromHexString(Detail::GetStringView(json));
|
||||
}
|
||||
|
||||
bool CanConvert(const Json::Value& json)
|
||||
{
|
||||
if (!json.isString())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto string{ Detail::GetStringView(json) };
|
||||
return (string.length() == 7 || string.length() == 3) && string.front() == '#';
|
||||
}
|
||||
};
|
||||
|
||||
template<typename T, typename TBase>
|
||||
struct EnumMapper
|
||||
{
|
||||
using pair_type = std::pair<std::string_view, T>;
|
||||
T FromJson(const Json::Value& json)
|
||||
{
|
||||
const auto name{ Detail::GetStringView(json) };
|
||||
for (const auto& pair : TBase::mappings)
|
||||
{
|
||||
if (pair.first == name)
|
||||
{
|
||||
return pair.second;
|
||||
}
|
||||
}
|
||||
|
||||
throw UnexpectedValueException{ name };
|
||||
}
|
||||
|
||||
bool CanConvert(const Json::Value& json)
|
||||
{
|
||||
return json.isString();
|
||||
}
|
||||
};
|
||||
|
||||
// FlagMapper is EnumMapper, but it works for bitfields.
|
||||
// It supports a string (single flag) or an array of strings.
|
||||
// Does an O(n*m) search; meant for small search spaces!
|
||||
//
|
||||
// Cleverly leverage EnumMapper to do the heavy lifting.
|
||||
template<typename T, typename TBase>
|
||||
struct FlagMapper : public EnumMapper<T, TBase>
|
||||
{
|
||||
static constexpr T AllSet{ static_cast<T>(~0u) };
|
||||
static constexpr T AllClear{ static_cast<T>(0u) };
|
||||
|
||||
T FromJson(const Json::Value& json)
|
||||
{
|
||||
if (json.isString())
|
||||
{
|
||||
return EnumMapper::FromJson(json);
|
||||
}
|
||||
else if (json.isArray())
|
||||
{
|
||||
unsigned int seen{ 0 };
|
||||
T value{};
|
||||
for (const auto& element : json)
|
||||
{
|
||||
const auto newFlag{ EnumMapper::FromJson(element) };
|
||||
if (++seen > 1 &&
|
||||
((newFlag == AllClear && value != AllClear) ||
|
||||
(value == AllClear && newFlag != AllClear)))
|
||||
{
|
||||
// attempt to combine AllClear (explicitly) with anything else
|
||||
throw UnexpectedValueException{ element.asString() };
|
||||
}
|
||||
value |= newFlag;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// We'll only get here if CanConvert has failed us.
|
||||
return AllClear;
|
||||
}
|
||||
|
||||
bool CanConvert(const Json::Value& json)
|
||||
{
|
||||
return EnumMapper::CanConvert(json) || json.isArray();
|
||||
}
|
||||
};
|
||||
|
||||
// Method Description:
|
||||
// - Helper that will populate a reference with a value converted from a json object.
|
||||
// Arguments:
|
||||
// - json: the json object to convert
|
||||
// - target: the value to populate with the converted result
|
||||
// Return Value:
|
||||
// - a boolean indicating whether the value existed (in this case, was non-null)
|
||||
//
|
||||
// GetValue, type-deduced, manual converter
|
||||
template<typename T, typename Converter>
|
||||
bool GetValue(const Json::Value& json, T& target, Converter&& conv)
|
||||
{
|
||||
if (json)
|
||||
{
|
||||
if (!conv.CanConvert(json))
|
||||
{
|
||||
throw TypeMismatchException{};
|
||||
}
|
||||
|
||||
target = conv.FromJson(json);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Overload on GetValue that will populate a std::optional with a value converted from json
|
||||
// - If the json value doesn't exist we'll leave the target object unmodified.
|
||||
// - If the json object is set to `null`, then
|
||||
// we'll instead set the target back to nullopt.
|
||||
// Arguments:
|
||||
// - json: the json object to convert
|
||||
// - target: the value to populate with the converted result
|
||||
// Return Value:
|
||||
// - a boolean indicating whether the optional was changed
|
||||
//
|
||||
// GetValue, type-deduced for optional, manual converter
|
||||
template<typename TOpt, typename Converter>
|
||||
bool GetValue(const Json::Value& json, std::optional<TOpt>& target, Converter&& conv)
|
||||
{
|
||||
if (json.isNull())
|
||||
{
|
||||
target = std::nullopt;
|
||||
return true; // null is valid for optionals
|
||||
}
|
||||
|
||||
std::decay_t<TOpt> local{};
|
||||
if (GetValue(json, local, std::forward<Converter>(conv)))
|
||||
{
|
||||
target = std::move(local);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// GetValue, forced return type, manual converter
|
||||
template<typename T, typename Converter>
|
||||
std::decay_t<T> GetValue(const Json::Value& json, Converter&& conv)
|
||||
{
|
||||
std::decay_t<T> local{};
|
||||
GetValue(json, local, std::forward<Converter>(conv));
|
||||
return local; // returns zero-initialized or value
|
||||
}
|
||||
|
||||
// GetValueForKey, type-deduced, manual converter
|
||||
template<typename T, typename Converter>
|
||||
bool GetValueForKey(const Json::Value& json, std::string_view key, T& target, Converter&& conv)
|
||||
{
|
||||
if (auto found{ json.find(&*key.cbegin(), (&*key.cbegin()) + key.size()) })
|
||||
{
|
||||
try
|
||||
{
|
||||
return GetValue(*found, target, std::forward<Converter>(conv));
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// Wrap any caught exceptions in one that preserves context.
|
||||
throw KeyedException(key, std::current_exception());
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// GetValueForKey, forced return type, manual converter
|
||||
template<typename T, typename Converter>
|
||||
std::decay_t<T> GetValueForKey(const Json::Value& json, std::string_view key, Converter&& conv)
|
||||
{
|
||||
std::decay_t<T> local{};
|
||||
GetValueForKey(json, key, local, std::forward<Converter>(conv));
|
||||
return local; // returns zero-initialized?
|
||||
}
|
||||
|
||||
// GetValue, type-deduced, with automatic converter
|
||||
template<typename T>
|
||||
bool GetValue(const Json::Value& json, T& target)
|
||||
{
|
||||
return GetValue(json, target, ConversionTrait<typename Detail::DeduceOptional<T>::Type>{});
|
||||
}
|
||||
|
||||
// GetValue, forced return type, with automatic converter
|
||||
template<typename T>
|
||||
std::decay_t<T> GetValue(const Json::Value& json)
|
||||
{
|
||||
std::decay_t<T> local{};
|
||||
GetValue(json, local, ConversionTrait<typename Detail::DeduceOptional<T>::Type>{});
|
||||
return local; // returns zero-initialized or value
|
||||
}
|
||||
|
||||
// GetValueForKey, type-deduced, with automatic converter
|
||||
template<typename T>
|
||||
bool GetValueForKey(const Json::Value& json, std::string_view key, T& target)
|
||||
{
|
||||
return GetValueForKey(json, key, target, ConversionTrait<typename Detail::DeduceOptional<T>::Type>{});
|
||||
}
|
||||
|
||||
// GetValueForKey, forced return type, with automatic converter
|
||||
template<typename T>
|
||||
std::decay_t<T> GetValueForKey(const Json::Value& json, std::string_view key)
|
||||
{
|
||||
return GetValueForKey<T>(json, key, ConversionTrait<typename Detail::DeduceOptional<T>::Type>{});
|
||||
}
|
||||
|
||||
// Get multiple values for keys (json, k, &v, k, &v, k, &v, ...).
|
||||
// Uses the default converter for each v.
|
||||
// Careful: this can cause a template explosion.
|
||||
constexpr void GetValuesForKeys(const Json::Value& /*json*/) {}
|
||||
|
||||
template<typename T, typename... Args>
|
||||
void GetValuesForKeys(const Json::Value& json, std::string_view key1, T&& val1, Args&&... args)
|
||||
{
|
||||
GetValueForKey(json, key1, val1);
|
||||
GetValuesForKeys(json, std::forward<Args>(args)...);
|
||||
}
|
||||
};
|
||||
|
||||
#define JSON_ENUM_MAPPER(...) \
|
||||
template<> \
|
||||
struct ::TerminalApp::JsonUtils::ConversionTrait<__VA_ARGS__> : \
|
||||
public ::TerminalApp::JsonUtils::EnumMapper<__VA_ARGS__, ::TerminalApp::JsonUtils::ConversionTrait<__VA_ARGS__>>
|
||||
|
||||
#define JSON_FLAG_MAPPER(...) \
|
||||
template<> \
|
||||
struct ::TerminalApp::JsonUtils::ConversionTrait<__VA_ARGS__> : \
|
||||
public ::TerminalApp::JsonUtils::FlagMapper<__VA_ARGS__, ::TerminalApp::JsonUtils::ConversionTrait<__VA_ARGS__>>
|
||||
|
||||
#define JSON_MAPPINGS(Count) \
|
||||
static constexpr std::array<pair_type, Count> mappings
|
|
@ -45,6 +45,16 @@ namespace winrt::TerminalApp::implementation
|
|||
void Tab::_MakeTabViewItem()
|
||||
{
|
||||
_tabViewItem = ::winrt::MUX::Controls::TabViewItem{};
|
||||
|
||||
_tabViewItem.DoubleTapped([weakThis = get_weak()](auto&& /*s*/, auto&& /*e*/) {
|
||||
if (auto tab{ weakThis.get() })
|
||||
{
|
||||
tab->_inRename = true;
|
||||
tab->_UpdateTabHeader();
|
||||
}
|
||||
});
|
||||
|
||||
_UpdateTitle();
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
|
|
|
@ -682,7 +682,8 @@ namespace winrt::TerminalApp::implementation
|
|||
const bool altPressed = WI_IsFlagSet(lAltState, CoreVirtualKeyStates::Down) ||
|
||||
WI_IsFlagSet(rAltState, CoreVirtualKeyStates::Down);
|
||||
|
||||
_LaunchSettings(altPressed);
|
||||
const auto target = altPressed ? SettingsTarget::DefaultsFile : SettingsTarget::SettingsFile;
|
||||
_LaunchSettings(target);
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
|
@ -1545,7 +1546,7 @@ namespace winrt::TerminalApp::implementation
|
|||
// - Called when the settings button is clicked. ShellExecutes the settings
|
||||
// file, as to open it in the default editor for .json files. Does this in
|
||||
// a background thread, as to not hang/crash the UI thread.
|
||||
fire_and_forget TerminalPage::_LaunchSettings(const bool openDefaults)
|
||||
fire_and_forget TerminalPage::_LaunchSettings(const SettingsTarget target)
|
||||
{
|
||||
// This will switch the execution of the function to a background (not
|
||||
// UI) thread. This is IMPORTANT, because the Windows.Storage API's
|
||||
|
@ -1553,13 +1554,26 @@ namespace winrt::TerminalApp::implementation
|
|||
// thread, because the main thread is a STA.
|
||||
co_await winrt::resume_background();
|
||||
|
||||
const auto settingsPath = openDefaults ? CascadiaSettings::GetDefaultSettingsPath() :
|
||||
CascadiaSettings::GetSettingsPath();
|
||||
auto openFile = [](const auto& filePath) {
|
||||
HINSTANCE res = ShellExecute(nullptr, nullptr, filePath.c_str(), nullptr, nullptr, SW_SHOW);
|
||||
if (static_cast<int>(reinterpret_cast<uintptr_t>(res)) <= 32)
|
||||
{
|
||||
ShellExecute(nullptr, nullptr, L"notepad", filePath.c_str(), nullptr, SW_SHOW);
|
||||
}
|
||||
};
|
||||
|
||||
HINSTANCE res = ShellExecute(nullptr, nullptr, settingsPath.c_str(), nullptr, nullptr, SW_SHOW);
|
||||
if (static_cast<int>(reinterpret_cast<uintptr_t>(res)) <= 32)
|
||||
switch (target)
|
||||
{
|
||||
ShellExecute(nullptr, nullptr, L"notepad", settingsPath.c_str(), nullptr, SW_SHOW);
|
||||
case SettingsTarget::DefaultsFile:
|
||||
openFile(CascadiaSettings::GetDefaultSettingsPath());
|
||||
break;
|
||||
case SettingsTarget::SettingsFile:
|
||||
openFile(CascadiaSettings::GetSettingsPath());
|
||||
break;
|
||||
case SettingsTarget::AllFiles:
|
||||
openFile(CascadiaSettings::GetDefaultSettingsPath());
|
||||
openFile(CascadiaSettings::GetSettingsPath());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -151,7 +151,7 @@ namespace winrt::TerminalApp::implementation
|
|||
void _PasteText();
|
||||
static fire_and_forget PasteFromClipboard(winrt::Microsoft::Terminal::TerminalControl::PasteFromClipboardEventArgs eventArgs);
|
||||
|
||||
fire_and_forget _LaunchSettings(const bool openDefaults);
|
||||
fire_and_forget _LaunchSettings(const winrt::TerminalApp::SettingsTarget target);
|
||||
|
||||
void _OnTabClick(const IInspectable& sender, const Windows::UI::Xaml::Input::PointerRoutedEventArgs& eventArgs);
|
||||
void _OnTabSelectionChanged(const IInspectable& sender, const Windows::UI::Xaml::Controls::SelectionChangedEventArgs& eventArgs);
|
||||
|
|
|
@ -76,7 +76,7 @@ std::vector<TerminalApp::Profile> WslDistroGenerator::GenerateProfiles()
|
|||
THROW_HR(ERROR_UNHANDLED_EXCEPTION);
|
||||
}
|
||||
DWORD exitCode;
|
||||
if (GetExitCodeProcess(pi.hProcess, &exitCode) == false)
|
||||
if (!GetExitCodeProcess(pi.hProcess, &exitCode))
|
||||
{
|
||||
THROW_HR(E_INVALIDARG);
|
||||
}
|
||||
|
@ -116,7 +116,7 @@ std::vector<TerminalApp::Profile> WslDistroGenerator::GenerateProfiles()
|
|||
continue;
|
||||
}
|
||||
|
||||
size_t firstChar = distName.find_first_of(L"( ");
|
||||
const size_t firstChar = distName.find_first_of(L"( ");
|
||||
// Some localizations don't have a space between the name and "(Default)"
|
||||
// https://github.com/microsoft/terminal/issues/1168#issuecomment-500187109
|
||||
if (firstChar < distName.size())
|
||||
|
|
|
@ -279,6 +279,7 @@
|
|||
{ "command": "toggleFullscreen", "keys": "f11" },
|
||||
{ "command": "openNewTabDropdown", "keys": "ctrl+shift+space" },
|
||||
{ "command": "openSettings", "keys": "ctrl+," },
|
||||
{ "command": { "action": "openSettings", "target": "defaultsFile" }, "keys": "ctrl+alt+," },
|
||||
{ "command": "find", "keys": "ctrl+shift+f" },
|
||||
|
||||
// Tab Management
|
||||
|
|
|
@ -201,7 +201,7 @@
|
|||
<ItemGroup>
|
||||
<!-- If you add idl files here, make sure to include their implementation's
|
||||
header in TerminalApp.vcxproj (as well as in this file) -->
|
||||
<Midl Include="../IF7Listener.idl" />
|
||||
<Midl Include="../IDirectKeyListener.idl" />
|
||||
<Midl Include="../App.idl">
|
||||
<DependentUpon>../App.xaml</DependentUpon>
|
||||
</Midl>
|
||||
|
@ -233,7 +233,8 @@
|
|||
</ItemGroup>
|
||||
<!-- ========================= Misc Files ======================== -->
|
||||
<ItemGroup>
|
||||
<PRIResource Include="..\Resources\*\Resources.resw" />
|
||||
<PRIResource Include="..\Resources\en-US\Resources.resw" />
|
||||
<OCResourceDirectory Include="../Resources" />
|
||||
<None Include="../packages.config" />
|
||||
</ItemGroup>
|
||||
<!-- ========================= Project References ======================== -->
|
||||
|
@ -345,4 +346,6 @@
|
|||
<Target Name="_TerminalAppGenerateUserSettingsH" Inputs="..\userDefaults.json" Outputs="Generated Files\userDefaults.h" BeforeTargets="BeforeClCompile">
|
||||
<Exec Command="powershell.exe -noprofile –ExecutionPolicy Unrestricted $(OpenConsoleDir)\tools\GenerateHeaderForJson.ps1 -JsonFile ..\userDefaults.json -OutPath '"Generated Files\userDefaults.h"' -VariableName UserSettingsJson" />
|
||||
</Target>
|
||||
|
||||
<Import Project="$(SolutionDir)build\rules\CollectWildcardResources.targets" />
|
||||
</Project>
|
||||
|
|
|
@ -223,7 +223,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
|
|||
try
|
||||
{
|
||||
const COORD dimensions{ gsl::narrow_cast<SHORT>(_initialCols), gsl::narrow_cast<SHORT>(_initialRows) };
|
||||
THROW_IF_FAILED(_CreatePseudoConsoleAndPipes(dimensions, PSEUDOCONSOLE_RESIZE_QUIRK, &_inPipe, &_outPipe, &_hPC));
|
||||
THROW_IF_FAILED(_CreatePseudoConsoleAndPipes(dimensions, PSEUDOCONSOLE_RESIZE_QUIRK | PSEUDOCONSOLE_WIN32_INPUT_MODE, &_inPipe, &_outPipe, &_hPC));
|
||||
THROW_IF_FAILED(_LaunchAttachedClient());
|
||||
|
||||
_startTime = std::chrono::high_resolution_clock::now();
|
||||
|
|
|
@ -59,7 +59,8 @@
|
|||
<Midl Include="TelnetConnection.idl" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PRIResource Include="Resources\*\Resources.resw" />
|
||||
<PRIResource Include="Resources\en-US\Resources.resw" />
|
||||
<OCResourceDirectory Include="Resources" />
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<!-- ========================= Project References ======================== -->
|
||||
|
@ -95,4 +96,6 @@
|
|||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<Import Project="..\..\..\packages\vcpkg-telnetpp.1.0.1\build\native\vcpkg-telnetpp.targets" Condition="Exists('..\..\..\packages\vcpkg-telnetpp.1.0.1\build\native\vcpkg-telnetpp.targets')" />
|
||||
|
||||
<Import Project="$(SolutionDir)build\rules\CollectWildcardResources.targets" />
|
||||
</Project>
|
||||
|
|
|
@ -27,6 +27,13 @@ using namespace winrt::Windows::System;
|
|||
using namespace winrt::Microsoft::Terminal::Settings;
|
||||
using namespace winrt::Windows::ApplicationModel::DataTransfer;
|
||||
|
||||
// The minimum delay between updates to the scroll bar's values.
|
||||
// The updates are throttled to limit power usage.
|
||||
constexpr const auto ScrollBarUpdateInterval = std::chrono::milliseconds(8);
|
||||
|
||||
// The minimum delay between updating the TSF input control.
|
||||
constexpr const auto TsfRedrawInterval = std::chrono::milliseconds(100);
|
||||
|
||||
namespace winrt::Microsoft::Terminal::TerminalControl::implementation
|
||||
{
|
||||
// Helper static function to ensure that all ambiguous-width glyphs are reported as narrow.
|
||||
|
@ -58,7 +65,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
|
|||
_initializedTerminal{ false },
|
||||
_settings{ settings },
|
||||
_closing{ false },
|
||||
_isTerminalInitiatedScroll{ false },
|
||||
_isInternalScrollBarUpdate{ false },
|
||||
_autoScrollVelocity{ 0 },
|
||||
_autoScrollingPointerPoint{ std::nullopt },
|
||||
_autoScrollTimer{},
|
||||
|
@ -98,6 +105,8 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
|
|||
auto inputFn = std::bind(&TermControl::_SendInputToConnection, this, std::placeholders::_1);
|
||||
_terminal->SetWriteInputCallback(inputFn);
|
||||
|
||||
_terminal->UpdateSettings(settings);
|
||||
|
||||
// Subscribe to the connection's disconnected event and call our connection closed handlers.
|
||||
_connectionStateChangedRevoker = _connection.StateChanged(winrt::auto_revoke, [this](auto&& /*s*/, auto&& /*v*/) {
|
||||
_ConnectionStateChangedHandlers(*this, nullptr);
|
||||
|
@ -118,6 +127,37 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
|
|||
}
|
||||
});
|
||||
|
||||
_tsfTryRedrawCanvas = std::make_shared<ThrottledFunc<>>(
|
||||
[weakThis = get_weak()]() {
|
||||
if (auto control{ weakThis.get() })
|
||||
{
|
||||
control->TSFInputControl().TryRedrawCanvas();
|
||||
}
|
||||
},
|
||||
TsfRedrawInterval,
|
||||
Dispatcher());
|
||||
|
||||
_updateScrollBar = std::make_shared<ThrottledFunc<ScrollBarUpdate>>(
|
||||
[weakThis = get_weak()](const auto& update) {
|
||||
if (auto control{ weakThis.get() })
|
||||
{
|
||||
control->_isInternalScrollBarUpdate = true;
|
||||
|
||||
auto scrollBar = control->ScrollBar();
|
||||
if (update.newValue.has_value())
|
||||
{
|
||||
scrollBar.Value(update.newValue.value());
|
||||
}
|
||||
scrollBar.Maximum(update.newMaximum);
|
||||
scrollBar.Minimum(update.newMinimum);
|
||||
scrollBar.ViewportSize(update.newViewportSize);
|
||||
|
||||
control->_isInternalScrollBarUpdate = false;
|
||||
}
|
||||
},
|
||||
ScrollBarUpdateInterval,
|
||||
Dispatcher());
|
||||
|
||||
static constexpr auto AutoScrollUpdateInterval = std::chrono::microseconds(static_cast<int>(1.0 / 30.0 * 1000000));
|
||||
_autoScrollTimer.Interval(AutoScrollUpdateInterval);
|
||||
_autoScrollTimer.Tick({ this, &TermControl::_UpdateAutoScroll });
|
||||
|
@ -212,20 +252,38 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
|
|||
{
|
||||
if (_closing)
|
||||
{
|
||||
return;
|
||||
co_return;
|
||||
}
|
||||
|
||||
// Update our control settings
|
||||
_ApplyUISettings();
|
||||
|
||||
// Update DxEngine's SelectionBackground
|
||||
_renderEngine->SetSelectionBackground(_settings.SelectionBackground());
|
||||
|
||||
// Update the terminal core with its new Core settings
|
||||
_terminal->UpdateSettings(_settings);
|
||||
|
||||
auto lock = _terminal->LockForWriting();
|
||||
|
||||
// Update DxEngine settings under the lock
|
||||
_renderEngine->SetSelectionBackground(_settings.SelectionBackground());
|
||||
|
||||
_renderEngine->SetRetroTerminalEffects(_settings.RetroTerminalEffect());
|
||||
_renderEngine->SetForceFullRepaintRendering(_settings.ForceFullRepaintRendering());
|
||||
_renderEngine->SetSoftwareRendering(_settings.SoftwareRendering());
|
||||
|
||||
switch (_settings.AntialiasingMode())
|
||||
{
|
||||
case TextAntialiasingMode::Cleartype:
|
||||
_renderEngine->SetAntialiasingMode(D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE);
|
||||
break;
|
||||
case TextAntialiasingMode::Aliased:
|
||||
_renderEngine->SetAntialiasingMode(D2D1_TEXT_ANTIALIAS_MODE_ALIASED);
|
||||
break;
|
||||
case TextAntialiasingMode::Grayscale:
|
||||
default:
|
||||
_renderEngine->SetAntialiasingMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE);
|
||||
break;
|
||||
}
|
||||
|
||||
// Refresh our font with the renderer
|
||||
const auto actualFontOldSize = _actualFont.GetSize();
|
||||
_UpdateFont();
|
||||
|
@ -579,13 +637,10 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
|
|||
|
||||
_terminal->CreateFromSettings(_settings, renderTarget);
|
||||
|
||||
// TODO:GH#3927 - Make it possible to hot-reload these settings. Right
|
||||
// here, the setting will only be used when the Terminal is initialized.
|
||||
dxEngine->SetRetroTerminalEffects(_settings.RetroTerminalEffect());
|
||||
dxEngine->SetForceFullRepaintRendering(_settings.ForceFullRepaintRendering());
|
||||
dxEngine->SetSoftwareRendering(_settings.SoftwareRendering());
|
||||
|
||||
// TODO:GH#3927 - hot-reload this one too
|
||||
// Update DxEngine's AntialiasingMode
|
||||
switch (_settings.AntialiasingMode())
|
||||
{
|
||||
|
@ -681,39 +736,64 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
|
|||
}
|
||||
|
||||
// Method Description:
|
||||
// - Manually generate an F7 event into the key bindings or terminal.
|
||||
// This is required as part of GH#638.
|
||||
// - Manually handles key events for certain keys that can't be passed to us
|
||||
// normally. Namely, the keys we're concerned with are F7 down and Alt up.
|
||||
// Return value:
|
||||
// - Whether F7 was handled.
|
||||
bool TermControl::OnF7Pressed()
|
||||
// - Whether the key was handled.
|
||||
bool TermControl::OnDirectKeyEvent(const uint32_t vkey, const bool down)
|
||||
{
|
||||
bool handled{ false };
|
||||
auto bindings{ _settings.KeyBindings() };
|
||||
|
||||
const auto modifiers{ _GetPressedModifierKeys() };
|
||||
|
||||
if (bindings)
|
||||
auto handled = false;
|
||||
if (vkey == VK_MENU && !down)
|
||||
{
|
||||
handled = bindings.TryKeyChord({
|
||||
modifiers.IsCtrlPressed(),
|
||||
modifiers.IsAltPressed(),
|
||||
modifiers.IsShiftPressed(),
|
||||
VK_F7,
|
||||
});
|
||||
}
|
||||
|
||||
if (!handled)
|
||||
{
|
||||
// _TrySendKeyEvent pretends it didn't handle F7 for some unknown reason.
|
||||
(void)_TrySendKeyEvent(VK_F7, 0, modifiers);
|
||||
// Manually generate an Alt KeyUp event into the key bindings or terminal.
|
||||
// This is required as part of GH#6421.
|
||||
// GH#6513 - make sure to set the scancode too, otherwise conpty
|
||||
// will think this is a NUL
|
||||
(void)_TrySendKeyEvent(VK_MENU, LOWORD(MapVirtualKeyW(VK_MENU, MAPVK_VK_TO_VSC)), modifiers, false);
|
||||
handled = true;
|
||||
}
|
||||
else if (vkey == VK_F7 && down)
|
||||
{
|
||||
// Manually generate an F7 event into the key bindings or terminal.
|
||||
// This is required as part of GH#638.
|
||||
auto bindings{ _settings.KeyBindings() };
|
||||
|
||||
if (bindings)
|
||||
{
|
||||
handled = bindings.TryKeyChord({
|
||||
modifiers.IsCtrlPressed(),
|
||||
modifiers.IsAltPressed(),
|
||||
modifiers.IsShiftPressed(),
|
||||
VK_F7,
|
||||
});
|
||||
}
|
||||
|
||||
if (!handled)
|
||||
{
|
||||
// _TrySendKeyEvent pretends it didn't handle F7 for some unknown reason.
|
||||
(void)_TrySendKeyEvent(VK_F7, 0, modifiers, true);
|
||||
// GH#6438: Note that we're _not_ sending the key up here - that'll
|
||||
// get passed through XAML to our KeyUp handler normally.
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
return handled;
|
||||
}
|
||||
|
||||
void TermControl::_KeyDownHandler(winrt::Windows::Foundation::IInspectable const& /*sender*/,
|
||||
Input::KeyRoutedEventArgs const& e)
|
||||
{
|
||||
_KeyHandler(e, true);
|
||||
}
|
||||
|
||||
void TermControl::_KeyUpHandler(winrt::Windows::Foundation::IInspectable const& /*sender*/,
|
||||
Input::KeyRoutedEventArgs const& e)
|
||||
{
|
||||
_KeyHandler(e, false);
|
||||
}
|
||||
|
||||
void TermControl::_KeyHandler(Input::KeyRoutedEventArgs const& e, const bool keyDown)
|
||||
{
|
||||
// If the current focused element is a child element of searchbox,
|
||||
// we do not send this event up to terminal
|
||||
|
@ -722,14 +802,15 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
|
|||
return;
|
||||
}
|
||||
|
||||
// mark event as handled and do nothing if...
|
||||
// - closing
|
||||
// - key modifier is pressed
|
||||
// NOTE: for key combos like CTRL + C, two events are fired (one for CTRL, one for 'C'). We care about the 'C' event and then check for key modifiers below.
|
||||
// Mark the event as handled and do nothing if we're closing, or the key
|
||||
// was the Windows key.
|
||||
//
|
||||
// NOTE: for key combos like CTRL + C, two events are fired (one for
|
||||
// CTRL, one for 'C'). Since it's possible the terminal is in
|
||||
// win32-input-mode, then we'll send all these keystrokes to the
|
||||
// terminal - it's smart enough to ignore the keys it doesn't care
|
||||
// about.
|
||||
if (_closing ||
|
||||
e.OriginalKey() == VirtualKey::Control ||
|
||||
e.OriginalKey() == VirtualKey::Shift ||
|
||||
e.OriginalKey() == VirtualKey::Menu ||
|
||||
e.OriginalKey() == VirtualKey::LeftWindows ||
|
||||
e.OriginalKey() == VirtualKey::RightWindows)
|
||||
|
||||
|
@ -741,48 +822,65 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
|
|||
const auto modifiers = _GetPressedModifierKeys();
|
||||
const auto vkey = gsl::narrow_cast<WORD>(e.OriginalKey());
|
||||
const auto scanCode = gsl::narrow_cast<WORD>(e.KeyStatus().ScanCode);
|
||||
bool handled = false;
|
||||
|
||||
// Alt-Numpad# input will send us a character once the user releases Alt, so we should be ignoring the individual keydowns.
|
||||
// The character will be sent through the TSFInputControl.
|
||||
// See GH#1401 for more details
|
||||
if (modifiers.IsAltPressed() && (e.OriginalKey() >= VirtualKey::NumberPad0 && e.OriginalKey() <= VirtualKey::NumberPad9))
|
||||
// Alt-Numpad# input will send us a character once the user releases
|
||||
// Alt, so we should be ignoring the individual keydowns. The character
|
||||
// will be sent through the TSFInputControl. See GH#1401 for more
|
||||
// details
|
||||
if (modifiers.IsAltPressed() &&
|
||||
(e.OriginalKey() >= VirtualKey::NumberPad0 && e.OriginalKey() <= VirtualKey::NumberPad9))
|
||||
|
||||
{
|
||||
e.Handled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// GH#2235: Terminal::Settings hasn't been modified to differentiate between AltGr and Ctrl+Alt yet.
|
||||
// GH#2235: Terminal::Settings hasn't been modified to differentiate
|
||||
// between AltGr and Ctrl+Alt yet.
|
||||
// -> Don't check for key bindings if this is an AltGr key combination.
|
||||
if (!modifiers.IsAltGrPressed())
|
||||
//
|
||||
// GH#4999: Only process keybindings on the keydown. If we don't check
|
||||
// this at all, we'll process the keybinding twice. If we only process
|
||||
// keybindings on the keyUp, then we'll still send the keydown to the
|
||||
// connected terminal application, and something like ctrl+shift+T will
|
||||
// emit a ^T to the pipe.
|
||||
if (!modifiers.IsAltGrPressed() && keyDown && _TryHandleKeyBinding(vkey, modifiers))
|
||||
{
|
||||
auto bindings = _settings.KeyBindings();
|
||||
if (bindings)
|
||||
{
|
||||
handled = bindings.TryKeyChord({
|
||||
modifiers.IsCtrlPressed(),
|
||||
modifiers.IsAltPressed(),
|
||||
modifiers.IsShiftPressed(),
|
||||
vkey,
|
||||
});
|
||||
}
|
||||
e.Handled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!handled)
|
||||
if (_TrySendKeyEvent(vkey, scanCode, modifiers, keyDown))
|
||||
{
|
||||
handled = _TrySendKeyEvent(vkey, scanCode, modifiers);
|
||||
e.Handled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Manually prevent keyboard navigation with tab. We want to send tab to
|
||||
// the terminal, and we don't want to be able to escape focus of the
|
||||
// control with tab.
|
||||
if (e.OriginalKey() == VirtualKey::Tab)
|
||||
e.Handled(e.OriginalKey() == VirtualKey::Tab);
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Attempt to handle this key combination as a key binding
|
||||
// Arguments:
|
||||
// - vkey: The vkey of the key pressed.
|
||||
// - modifiers: The ControlKeyStates representing the modifier key states.
|
||||
bool TermControl::_TryHandleKeyBinding(const WORD vkey, ::Microsoft::Terminal::Core::ControlKeyStates modifiers) const
|
||||
{
|
||||
auto bindings = _settings.KeyBindings();
|
||||
if (!bindings)
|
||||
{
|
||||
handled = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
e.Handled(handled);
|
||||
return bindings.TryKeyChord({
|
||||
modifiers.IsCtrlPressed(),
|
||||
modifiers.IsAltPressed(),
|
||||
modifiers.IsShiftPressed(),
|
||||
vkey,
|
||||
});
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
|
@ -793,12 +891,19 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
|
|||
// Arguments:
|
||||
// - vkey: The vkey of the key pressed.
|
||||
// - states: The Microsoft::Terminal::Core::ControlKeyStates representing the modifier key states.
|
||||
bool TermControl::_TrySendKeyEvent(const WORD vkey, const WORD scanCode, const ControlKeyStates modifiers)
|
||||
// - keyDown: If true, the key was pressed, otherwise the key was released.
|
||||
bool TermControl::_TrySendKeyEvent(const WORD vkey,
|
||||
const WORD scanCode,
|
||||
const ControlKeyStates modifiers,
|
||||
const bool keyDown)
|
||||
{
|
||||
// When there is a selection active, escape should clear it and NOT flow through
|
||||
// to the terminal. With any other keypress, it should clear the selection AND
|
||||
// flow through to the terminal.
|
||||
if (_terminal->IsSelectionActive())
|
||||
// GH#6423 - don't dismiss selection if the key that was pressed was a
|
||||
// modifier key. We'll wait for a real keystroke to dismiss the
|
||||
// selection.
|
||||
if (_terminal->IsSelectionActive() && !KeyEvent::IsModifierKey(vkey))
|
||||
{
|
||||
_terminal->ClearSelection();
|
||||
_renderer->TriggerSelection();
|
||||
|
@ -818,7 +923,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
|
|||
// If the terminal translated the key, mark the event as handled.
|
||||
// This will prevent the system from trying to get the character out
|
||||
// of it and sending us a CharacterReceived event.
|
||||
const auto handled = vkey ? _terminal->SendKeyEvent(vkey, scanCode, modifiers) : true;
|
||||
const auto handled = vkey ? _terminal->SendKeyEvent(vkey, scanCode, modifiers, keyDown) : true;
|
||||
|
||||
if (_cursorTimer.has_value())
|
||||
{
|
||||
|
@ -1395,8 +1500,11 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
|
|||
void TermControl::_ScrollbarChangeHandler(Windows::Foundation::IInspectable const& /*sender*/,
|
||||
Controls::Primitives::RangeBaseValueChangedEventArgs const& args)
|
||||
{
|
||||
if (_isTerminalInitiatedScroll || _closing)
|
||||
if (_isInternalScrollBarUpdate || _closing)
|
||||
{
|
||||
// The update comes from ourselves, more specifically from the
|
||||
// terminal. So we don't have to update the terminal because it
|
||||
// already knows.
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1406,9 +1514,11 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
|
|||
// itself - it was initiated by the mouse wheel, or the scrollbar.
|
||||
_terminal->UserScrollViewport(newValue);
|
||||
|
||||
// We've just told the terminal to update its viewport to reflect the
|
||||
// new scroll value so the scroll bar matches the viewport now.
|
||||
_willUpdateScrollBarToMatchViewport.store(false);
|
||||
// User input takes priority over terminal events so cancel
|
||||
// any pending scroll bar update if the user scrolls.
|
||||
_updateScrollBar->ModifyPending([](auto& update) {
|
||||
update.newValue.reset();
|
||||
});
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
|
@ -1907,35 +2017,6 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
|
|||
_titleChangedHandlers(winrt::hstring{ wstr });
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Update the position and size of the scrollbar to match the given
|
||||
// viewport top, viewport height, and buffer size.
|
||||
// The change will be actually handled in _ScrollbarChangeHandler.
|
||||
// This should be done on the UI thread. Make sure the caller is calling
|
||||
// us in a RunAsync block.
|
||||
// Arguments:
|
||||
// - viewTop: the top of the visible viewport, in rows. 0 indicates the top
|
||||
// of the buffer.
|
||||
// - viewHeight: the height of the viewport in rows.
|
||||
// - bufferSize: the length of the buffer, in rows
|
||||
void TermControl::_ScrollbarUpdater(Controls::Primitives::ScrollBar scrollBar,
|
||||
const int viewTop,
|
||||
const int viewHeight,
|
||||
const int bufferSize)
|
||||
{
|
||||
// The terminal is already in the scroll position it wants, so no need
|
||||
// to tell it to scroll.
|
||||
_isTerminalInitiatedScroll = true;
|
||||
|
||||
const auto hiddenContent = bufferSize - viewHeight;
|
||||
scrollBar.Maximum(hiddenContent);
|
||||
scrollBar.Minimum(0);
|
||||
scrollBar.ViewportSize(viewHeight);
|
||||
scrollBar.Value(viewTop);
|
||||
|
||||
_isTerminalInitiatedScroll = false;
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Update the position and size of the scrollbar to match the given
|
||||
// viewport top, viewport height, and buffer size.
|
||||
|
@ -1946,9 +2027,9 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
|
|||
// of the buffer.
|
||||
// - viewHeight: the height of the viewport in rows.
|
||||
// - bufferSize: the length of the buffer, in rows
|
||||
winrt::fire_and_forget TermControl::_TerminalScrollPositionChanged(const int viewTop,
|
||||
const int viewHeight,
|
||||
const int bufferSize)
|
||||
void TermControl::_TerminalScrollPositionChanged(const int viewTop,
|
||||
const int viewHeight,
|
||||
const int bufferSize)
|
||||
{
|
||||
// Since this callback fires from non-UI thread, we might be already
|
||||
// closed/closing.
|
||||
|
@ -1959,21 +2040,14 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
|
|||
|
||||
_scrollPositionChangedHandlers(viewTop, viewHeight, bufferSize);
|
||||
|
||||
auto weakThis{ get_weak() };
|
||||
ScrollBarUpdate update;
|
||||
const auto hiddenContent = bufferSize - viewHeight;
|
||||
update.newMaximum = hiddenContent;
|
||||
update.newMinimum = 0;
|
||||
update.newViewportSize = viewHeight;
|
||||
update.newValue = viewTop;
|
||||
|
||||
co_await winrt::resume_foreground(Dispatcher());
|
||||
|
||||
// Even if we weren't closed/closing few lines above, we might be
|
||||
// while waiting for this block of code to be dispatched.
|
||||
// If 'weakThis' is locked, then we can safely work with 'this'
|
||||
if (auto control{ weakThis.get() })
|
||||
{
|
||||
if (!_closing.load())
|
||||
{
|
||||
// Update our scrollbar
|
||||
_ScrollbarUpdater(ScrollBar(), viewTop, viewHeight, bufferSize);
|
||||
}
|
||||
}
|
||||
_updateScrollBar->Run(update);
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
|
@ -1981,42 +2055,9 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
|
|||
// to be where the current cursor position is.
|
||||
// Arguments:
|
||||
// - N/A
|
||||
winrt::fire_and_forget TermControl::_TerminalCursorPositionChanged()
|
||||
void TermControl::_TerminalCursorPositionChanged()
|
||||
{
|
||||
bool expectedFalse{ false };
|
||||
if (!_coroutineDispatchStateUpdateInProgress.compare_exchange_weak(expectedFalse, true))
|
||||
{
|
||||
// somebody's already in here.
|
||||
return;
|
||||
}
|
||||
|
||||
if (_closing.load())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto dispatcher{ Dispatcher() }; // cache a strong ref to this in case TermControl dies
|
||||
auto weakThis{ get_weak() };
|
||||
|
||||
// Muffle 2: Muffle Harder
|
||||
// If we're the lucky coroutine who gets through, we'll still wait 100ms to clog
|
||||
// the atomic above so we don't service the cursor update too fast. If we get through
|
||||
// and finish processing the update quickly but similar requests are still beating
|
||||
// down the door above in the atomic, we may still update the cursor way more than
|
||||
// is visible to anyone's eye, which is a waste of effort.
|
||||
static constexpr auto CursorUpdateQuiesceTime{ std::chrono::milliseconds(100) };
|
||||
co_await winrt::resume_after(CursorUpdateQuiesceTime);
|
||||
|
||||
co_await winrt::resume_foreground(dispatcher);
|
||||
|
||||
if (auto control{ weakThis.get() })
|
||||
{
|
||||
if (!_closing.load())
|
||||
{
|
||||
TSFInputControl().TryRedrawCanvas();
|
||||
}
|
||||
_coroutineDispatchStateUpdateInProgress.store(false);
|
||||
}
|
||||
_tsfTryRedrawCanvas->Run();
|
||||
}
|
||||
|
||||
hstring TermControl::Title()
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
#include "../buffer/out/search.h"
|
||||
#include "cppwinrt_utils.h"
|
||||
#include "SearchBoxControl.h"
|
||||
#include "ThrottledFunc.h"
|
||||
|
||||
namespace winrt::Microsoft::Terminal::TerminalControl::implementation
|
||||
{
|
||||
|
@ -84,7 +85,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
|
|||
|
||||
void CreateSearchBoxControl();
|
||||
|
||||
bool OnF7Pressed();
|
||||
bool OnDirectKeyEvent(const uint32_t vkey, const bool down);
|
||||
|
||||
bool OnMouseWheel(const Windows::Foundation::Point location, const int32_t delta);
|
||||
|
||||
|
@ -135,8 +136,17 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
|
|||
FontInfoDesired _desiredFont;
|
||||
FontInfo _actualFont;
|
||||
|
||||
bool _isTerminalInitiatedScroll;
|
||||
std::atomic<bool> _willUpdateScrollBarToMatchViewport;
|
||||
std::shared_ptr<ThrottledFunc<>> _tsfTryRedrawCanvas;
|
||||
|
||||
struct ScrollBarUpdate
|
||||
{
|
||||
std::optional<double> newValue;
|
||||
double newMaximum;
|
||||
double newMinimum;
|
||||
double newViewportSize;
|
||||
};
|
||||
std::shared_ptr<ThrottledFunc<ScrollBarUpdate>> _updateScrollBar;
|
||||
bool _isInternalScrollBarUpdate;
|
||||
|
||||
int _rowsToScroll;
|
||||
|
||||
|
@ -180,6 +190,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
|
|||
void _SetFontSize(int fontSize);
|
||||
void _TappedHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::TappedRoutedEventArgs const& e);
|
||||
void _KeyDownHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e);
|
||||
void _KeyUpHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e);
|
||||
void _CharacterHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::CharacterReceivedRoutedEventArgs const& e);
|
||||
void _PointerPressedHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e);
|
||||
void _PointerMovedHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e);
|
||||
|
@ -200,8 +211,8 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
|
|||
void _DoResizeUnderLock(const double newWidth, const double newHeight);
|
||||
void _RefreshSizeUnderLock();
|
||||
void _TerminalTitleChanged(const std::wstring_view& wstr);
|
||||
winrt::fire_and_forget _TerminalScrollPositionChanged(const int viewTop, const int viewHeight, const int bufferSize);
|
||||
winrt::fire_and_forget _TerminalCursorPositionChanged();
|
||||
void _TerminalScrollPositionChanged(const int viewTop, const int viewHeight, const int bufferSize);
|
||||
void _TerminalCursorPositionChanged();
|
||||
|
||||
void _MouseScrollHandler(const double mouseDelta, const Windows::Foundation::Point point, const bool isLeftButtonPressed);
|
||||
void _MouseZoomHandler(const double delta);
|
||||
|
@ -215,11 +226,12 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
|
|||
void _TryStopAutoScroll(const uint32_t pointerId);
|
||||
void _UpdateAutoScroll(Windows::Foundation::IInspectable const& sender, Windows::Foundation::IInspectable const& e);
|
||||
|
||||
void _ScrollbarUpdater(Windows::UI::Xaml::Controls::Primitives::ScrollBar scrollbar, const int viewTop, const int viewHeight, const int bufferSize);
|
||||
static Windows::UI::Xaml::Thickness _ParseThicknessFromPadding(const hstring padding);
|
||||
|
||||
void _KeyHandler(Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e, const bool keyDown);
|
||||
::Microsoft::Terminal::Core::ControlKeyStates _GetPressedModifierKeys() const;
|
||||
bool _TrySendKeyEvent(const WORD vkey, const WORD scanCode, ::Microsoft::Terminal::Core::ControlKeyStates modifiers);
|
||||
bool _TryHandleKeyBinding(const WORD vkey, ::Microsoft::Terminal::Core::ControlKeyStates modifiers) const;
|
||||
bool _TrySendKeyEvent(const WORD vkey, const WORD scanCode, ::Microsoft::Terminal::Core::ControlKeyStates modifiers, const bool keyDown);
|
||||
bool _TrySendMouseEvent(Windows::UI::Input::PointerPoint const& point);
|
||||
bool _CanSendVTMouseInput();
|
||||
|
||||
|
@ -236,12 +248,6 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
|
|||
void _FontInfoHandler(const IInspectable& sender, const FontInfoEventArgs& eventArgs);
|
||||
|
||||
winrt::fire_and_forget _AsyncCloseConnection();
|
||||
|
||||
// this atomic is to be used as a guard against dispatching billions of coroutines for
|
||||
// routine state changes that might happen millions of times a second.
|
||||
// Unbounded main dispatcher use leads to massive memory leaks and intense slowdowns
|
||||
// on the UI thread.
|
||||
std::atomic<bool> _coroutineDispatchStateUpdateInProgress{ false };
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -11,13 +11,11 @@ namespace Microsoft.Terminal.TerminalControl
|
|||
|
||||
// C++/winrt makes it difficult to share this idl between two projects,
|
||||
// Instead, we just pin the uuid and include it in both TermControl and App
|
||||
// If you update this one, please update TerminalApp\IF7Listener.idl.
|
||||
// If you update this one, please update TerminalApp\IDirectKeyListener.idl.
|
||||
// If you change this interface, please update the guid.
|
||||
// If you press F7 and get a runtime error, go make sure both copies are the same.
|
||||
[uuid("339e1a87-5315-4da6-96f0-565549b6472b")]
|
||||
interface IF7Listener
|
||||
{
|
||||
Boolean OnF7Pressed();
|
||||
// If you press F7 or Alt and get a runtime error, go make sure both copies are the same.
|
||||
[uuid("339e1a87-5315-4da6-96f0-565549b6472b")] interface IDirectKeyListener {
|
||||
Boolean OnDirectKeyEvent(UInt32 vkey, Boolean down);
|
||||
}
|
||||
|
||||
runtimeclass CopyToClipboardEventArgs
|
||||
|
@ -32,7 +30,7 @@ namespace Microsoft.Terminal.TerminalControl
|
|||
void HandleClipboardData(String data);
|
||||
}
|
||||
|
||||
[default_interface] runtimeclass TermControl : Windows.UI.Xaml.Controls.UserControl, IF7Listener, IMouseWheelListener
|
||||
[default_interface] runtimeclass TermControl : Windows.UI.Xaml.Controls.UserControl, IDirectKeyListener, IMouseWheelListener
|
||||
{
|
||||
TermControl();
|
||||
TermControl(Microsoft.Terminal.Settings.IControlSettings settings, Microsoft.Terminal.TerminalConnection.ITerminalConnection connection);
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
Tapped="_TappedHandler"
|
||||
PointerWheelChanged="_MouseWheelHandler"
|
||||
PreviewKeyDown="_KeyDownHandler"
|
||||
KeyUp="_KeyUpHandler"
|
||||
CharacterReceived="_CharacterHandler"
|
||||
GotFocus="_GotFocusHandler"
|
||||
LostFocus="_LostFocusHandler">
|
||||
|
|
|
@ -43,6 +43,7 @@
|
|||
<ClInclude Include="TermControlAutomationPeer.h">
|
||||
<DependentUpon>TermControlAutomationPeer.idl</DependentUpon>
|
||||
</ClInclude>
|
||||
<ClInclude Include="ThrottledFunc.h" />
|
||||
<ClInclude Include="TSFInputControl.h">
|
||||
<DependentUpon>TSFInputControl.xaml</DependentUpon>
|
||||
</ClInclude>
|
||||
|
@ -59,6 +60,7 @@
|
|||
<ClCompile Include="TermControl.cpp">
|
||||
<DependentUpon>TermControl.xaml</DependentUpon>
|
||||
</ClCompile>
|
||||
<ClCompile Include="ThrottledFunc.cpp" />
|
||||
<ClCompile Include="TSFInputControl.cpp">
|
||||
<DependentUpon>TSFInputControl.xaml</DependentUpon>
|
||||
</ClCompile>
|
||||
|
@ -86,7 +88,8 @@
|
|||
<None Include="TerminalControl.def" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PRIResource Include="Resources\*\Resources.resw" />
|
||||
<PRIResource Include="Resources\en-US\Resources.resw" />
|
||||
<OCResourceDirectory Include="Resources" />
|
||||
</ItemGroup>
|
||||
<!-- ========================= Project References ======================== -->
|
||||
<ItemGroup>
|
||||
|
@ -133,4 +136,6 @@
|
|||
<AdditionalIncludeDirectories>$(OpenConsoleDir)src\cascadia\inc;$(OpenConsoleDir)src\types\inc;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
|
||||
<Import Project="$(SolutionDir)build\rules\CollectWildcardResources.targets" />
|
||||
</Project>
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
<ClCompile Include="UiaTextRange.cpp" />
|
||||
<ClCompile Include="SearchBoxControl.cpp" />
|
||||
<ClCompile Include="init.cpp" />
|
||||
<ClCompile Include="ThrottledFunc.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="pch.h" />
|
||||
|
@ -26,6 +27,7 @@
|
|||
<ClInclude Include="TermControlAutomationPeer.h" />
|
||||
<ClInclude Include="XamlUiaTextRange.h" />
|
||||
<ClInclude Include="TermControlUiaProvider.hpp" />
|
||||
<ClInclude Include="ThrottledFunc.h" />
|
||||
<ClInclude Include="UiaTextRange.hpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
@ -45,7 +47,7 @@
|
|||
<Natvis Include="$(SolutionDir)tools\ConsoleTypes.natvis" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PRIResource Include="Resources\*\Resources.resw">
|
||||
<PRIResource Include="Resources\en-US\Resources.resw">
|
||||
<Filter>Resources</Filter>
|
||||
</PRIResource>
|
||||
</ItemGroup>
|
||||
|
|
54
src/cascadia/TerminalControl/ThrottledFunc.cpp
Normal file
|
@ -0,0 +1,54 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
#include "pch.h"
|
||||
|
||||
#include "ThrottledFunc.h"
|
||||
|
||||
using namespace winrt::Windows::Foundation;
|
||||
using namespace winrt::Windows::UI::Core;
|
||||
using namespace winrt::Windows::UI::Xaml;
|
||||
|
||||
ThrottledFunc<>::ThrottledFunc(ThrottledFunc::Func func, TimeSpan delay, CoreDispatcher dispatcher) :
|
||||
_func{ func },
|
||||
_delay{ delay },
|
||||
_dispatcher{ dispatcher },
|
||||
_isRunPending{}
|
||||
{
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Runs the function later, except if `Run` is called again before
|
||||
// with a new argument, in which case the request will be ignored.
|
||||
// - For more information, read the class' documentation.
|
||||
// - This method is always thread-safe. It can be called multiple times on
|
||||
// different threads.
|
||||
// Arguments:
|
||||
// - <none>
|
||||
// Return Value:
|
||||
// - <none>
|
||||
void ThrottledFunc<>::Run()
|
||||
{
|
||||
if (_isRunPending.test_and_set())
|
||||
{
|
||||
// already pending
|
||||
return;
|
||||
}
|
||||
|
||||
_dispatcher.RunAsync(CoreDispatcherPriority::Low, [weakThis = this->weak_from_this()]() {
|
||||
if (auto self{ weakThis.lock() })
|
||||
{
|
||||
DispatcherTimer timer;
|
||||
timer.Interval(self->_delay);
|
||||
timer.Tick([=](auto&&...) {
|
||||
if (auto self{ weakThis.lock() })
|
||||
{
|
||||
timer.Stop();
|
||||
self->_isRunPending.clear();
|
||||
self->_func();
|
||||
}
|
||||
});
|
||||
timer.Start();
|
||||
}
|
||||
});
|
||||
}
|
142
src/cascadia/TerminalControl/ThrottledFunc.h
Normal file
|
@ -0,0 +1,142 @@
|
|||
/*++
|
||||
Copyright (c) Microsoft Corporation
|
||||
Licensed under the MIT license.
|
||||
|
||||
Module Name:
|
||||
- ThrottledFunc.h
|
||||
--*/
|
||||
|
||||
#pragma once
|
||||
#include "pch.h"
|
||||
|
||||
// Class Description:
|
||||
// - Represents a function that takes arguments and whose invocation is
|
||||
// delayed by a specified duration and rate-limited such that if the code
|
||||
// tries to run the function while a call to the function is already
|
||||
// pending, then the previous call with the previous arguments will be
|
||||
// cancelled and the call will be made with the new arguments instead.
|
||||
// - The function will be run on the the specified dispatcher.
|
||||
template<typename... Args>
|
||||
class ThrottledFunc : public std::enable_shared_from_this<ThrottledFunc<Args...>>
|
||||
{
|
||||
public:
|
||||
using Func = std::function<void(Args...)>;
|
||||
|
||||
ThrottledFunc(Func func, winrt::Windows::Foundation::TimeSpan delay, winrt::Windows::UI::Core::CoreDispatcher dispatcher) :
|
||||
_func{ func },
|
||||
_delay{ delay },
|
||||
_dispatcher{ dispatcher }
|
||||
{
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Runs the function later with the specified arguments, except if `Run`
|
||||
// is called again before with new arguments, in which case the new
|
||||
// arguments will be used instead.
|
||||
// - For more information, read the class' documentation.
|
||||
// - This method is always thread-safe. It can be called multiple times on
|
||||
// different threads.
|
||||
// Arguments:
|
||||
// - arg: the argument to pass to the function
|
||||
// Return Value:
|
||||
// - <none>
|
||||
template<typename... MakeArgs>
|
||||
void Run(MakeArgs&&... args)
|
||||
{
|
||||
{
|
||||
std::lock_guard guard{ _lock };
|
||||
|
||||
bool hadValue = _pendingRunArgs.has_value();
|
||||
_pendingRunArgs.emplace(std::forward<MakeArgs>(args)...);
|
||||
|
||||
if (hadValue)
|
||||
{
|
||||
// already pending
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_dispatcher.RunAsync(CoreDispatcherPriority::Low, [weakThis = this->weak_from_this()]() {
|
||||
if (auto self{ weakThis.lock() })
|
||||
{
|
||||
DispatcherTimer timer;
|
||||
timer.Interval(self->_delay);
|
||||
timer.Tick([=](auto&&...) {
|
||||
if (auto self{ weakThis.lock() })
|
||||
{
|
||||
timer.Stop();
|
||||
|
||||
std::optional<std::tuple<Args...>> args;
|
||||
{
|
||||
std::lock_guard guard{ self->_lock };
|
||||
self->_pendingRunArgs.swap(args);
|
||||
}
|
||||
std::apply(self->_func, args.value());
|
||||
}
|
||||
});
|
||||
timer.Start();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Modifies the pending arguments for the next function invocation, if
|
||||
// there is one pending currently.
|
||||
// - Let's say that you just called the `Run` method with some arguments.
|
||||
// After the delay specified in the constructor, the function specified
|
||||
// in the constructor will be called with these arguments.
|
||||
// - By using this method, you can modify the arguments before the function
|
||||
// is called.
|
||||
// - You pass a function to this method which will take references to
|
||||
// the arguments (one argument corresponds to one reference to an
|
||||
// argument) and will modify them.
|
||||
// - When there is no pending invocation of the function, this method will
|
||||
// not do anything.
|
||||
// - This method is always thread-safe. It can be called multiple times on
|
||||
// different threads.
|
||||
// Arguments:
|
||||
// - f: the function to call with references to the arguments
|
||||
// Return Value:
|
||||
// - <none>
|
||||
template<typename F>
|
||||
void ModifyPending(F f)
|
||||
{
|
||||
std::lock_guard guard{ _lock };
|
||||
|
||||
if (_pendingRunArgs.has_value())
|
||||
{
|
||||
std::apply(f, _pendingRunArgs.value());
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
Func _func;
|
||||
winrt::Windows::Foundation::TimeSpan _delay;
|
||||
winrt::Windows::UI::Core::CoreDispatcher _dispatcher;
|
||||
|
||||
std::optional<std::tuple<Args...>> _pendingRunArgs;
|
||||
std::mutex _lock;
|
||||
};
|
||||
|
||||
// Class Description:
|
||||
// - Represents a function whose invocation is delayed by a specified duration
|
||||
// and rate-limited such that if the code tries to run the function while a
|
||||
// call to the function is already pending, the request will be ignored.
|
||||
// - The function will be run on the the specified dispatcher.
|
||||
template<>
|
||||
class ThrottledFunc<> : public std::enable_shared_from_this<ThrottledFunc<>>
|
||||
{
|
||||
public:
|
||||
using Func = std::function<void()>;
|
||||
|
||||
ThrottledFunc(Func func, winrt::Windows::Foundation::TimeSpan delay, winrt::Windows::UI::Core::CoreDispatcher dispatcher);
|
||||
|
||||
void Run();
|
||||
|
||||
private:
|
||||
Func _func;
|
||||
winrt::Windows::Foundation::TimeSpan _delay;
|
||||
winrt::Windows::UI::Core::CoreDispatcher _dispatcher;
|
||||
|
||||
std::atomic_flag _isRunPending;
|
||||
};
|
|
@ -49,6 +49,7 @@ namespace Microsoft::Terminal::Core
|
|||
virtual bool SetDefaultForeground(const DWORD color) noexcept = 0;
|
||||
virtual bool SetDefaultBackground(const DWORD color) noexcept = 0;
|
||||
|
||||
virtual bool EnableWin32InputMode(const bool win32InputMode) noexcept = 0;
|
||||
virtual bool SetCursorKeysMode(const bool applicationMode) noexcept = 0;
|
||||
virtual bool SetKeypadMode(const bool applicationMode) noexcept = 0;
|
||||
virtual bool EnableVT200MouseMode(const bool enabled) noexcept = 0;
|
||||
|
|
|
@ -16,7 +16,7 @@ namespace Microsoft::Terminal::Core
|
|||
ITerminalInput& operator=(const ITerminalInput&) = default;
|
||||
ITerminalInput& operator=(ITerminalInput&&) = default;
|
||||
|
||||
virtual bool SendKeyEvent(const WORD vkey, const WORD scanCode, const ControlKeyStates states) = 0;
|
||||
virtual bool SendKeyEvent(const WORD vkey, const WORD scanCode, const ControlKeyStates states, const bool keyDown) = 0;
|
||||
virtual bool SendMouseEvent(const COORD viewportPos, const unsigned int uiButton, const ControlKeyStates states, const short wheelDelta) = 0;
|
||||
virtual bool SendCharEvent(const wchar_t ch, const WORD scanCode, const ControlKeyStates states) = 0;
|
||||
|
||||
|
|
|
@ -44,6 +44,7 @@ Terminal::Terminal() :
|
|||
_pfnWriteInput{ nullptr },
|
||||
_scrollOffset{ 0 },
|
||||
_snapOnInput{ true },
|
||||
_altGrAliasing{ true },
|
||||
_blockSelection{ false },
|
||||
_selection{ std::nullopt }
|
||||
{
|
||||
|
@ -92,11 +93,6 @@ void Terminal::CreateFromSettings(winrt::Microsoft::Terminal::Settings::ICoreSet
|
|||
Create(viewportSize, Utils::ClampToShortMax(settings.HistorySize(), 0), renderTarget);
|
||||
|
||||
UpdateSettings(settings);
|
||||
|
||||
if (_suppressApplicationTitle)
|
||||
{
|
||||
_title = _startingTitle;
|
||||
}
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
|
@ -130,9 +126,12 @@ void Terminal::UpdateSettings(winrt::Microsoft::Terminal::Settings::ICoreSetting
|
|||
break;
|
||||
}
|
||||
|
||||
_buffer->GetCursor().SetStyle(settings.CursorHeight(),
|
||||
settings.CursorColor(),
|
||||
cursorShape);
|
||||
if (_buffer)
|
||||
{
|
||||
_buffer->GetCursor().SetStyle(settings.CursorHeight(),
|
||||
settings.CursorColor(),
|
||||
cursorShape);
|
||||
}
|
||||
|
||||
for (int i = 0; i < 16; i++)
|
||||
{
|
||||
|
@ -145,6 +144,8 @@ void Terminal::UpdateSettings(winrt::Microsoft::Terminal::Settings::ICoreSetting
|
|||
_suppressApplicationTitle = settings.SuppressApplicationTitle();
|
||||
_startingTitle = settings.StartingTitle();
|
||||
|
||||
_terminalInput->ForceDisableWin32InputMode(settings.ForceVTInput());
|
||||
|
||||
// TODO:MSFT:21327402 - if HistorySize has changed, resize the buffer so we
|
||||
// have a smaller scrollback. We should do this carefully - if the new buffer
|
||||
// size is smaller than where the mutable viewport currently is, we'll want
|
||||
|
@ -408,12 +409,24 @@ bool Terminal::IsTrackingMouseInput() const noexcept
|
|||
// - vkey: The vkey of the last pressed key.
|
||||
// - scanCode: The scan code of the last pressed key.
|
||||
// - states: The Microsoft::Terminal::Core::ControlKeyStates representing the modifier key states.
|
||||
// - keyDown: If true, the key was pressed, otherwise the key was released.
|
||||
// Return Value:
|
||||
// - true if we translated the key event, and it should not be processed any further.
|
||||
// - false if we did not translate the key, and it should be processed into a character.
|
||||
bool Terminal::SendKeyEvent(const WORD vkey, const WORD scanCode, const ControlKeyStates states)
|
||||
bool Terminal::SendKeyEvent(const WORD vkey,
|
||||
const WORD scanCode,
|
||||
const ControlKeyStates states,
|
||||
const bool keyDown)
|
||||
{
|
||||
TrySnapOnInput();
|
||||
// GH#6423 - don't snap on this key if the key that was pressed was a
|
||||
// modifier key. We'll wait for a real keystroke to snap to the bottom.
|
||||
// GH#6481 - Additionally, make sure the key was actually pressed. This
|
||||
// check will make sure we behave the same as before GH#6309
|
||||
if (!KeyEvent::IsModifierKey(vkey) && keyDown)
|
||||
{
|
||||
TrySnapOnInput();
|
||||
}
|
||||
|
||||
_StoreKeyEvent(vkey, scanCode);
|
||||
|
||||
const auto isAltOnlyPressed = states.IsAltPressed() && !states.IsCtrlPressed();
|
||||
|
@ -452,7 +465,7 @@ bool Terminal::SendKeyEvent(const WORD vkey, const WORD scanCode, const ControlK
|
|||
return false;
|
||||
}
|
||||
|
||||
KeyEvent keyEv{ true, 0, vkey, scanCode, ch, states.Value() };
|
||||
KeyEvent keyEv{ keyDown, 1, vkey, scanCode, ch, states.Value() };
|
||||
return _terminalInput->HandleKey(&keyEv);
|
||||
}
|
||||
|
||||
|
@ -513,8 +526,15 @@ bool Terminal::SendCharEvent(const wchar_t ch, const WORD scanCode, const Contro
|
|||
vkey = _VirtualKeyFromCharacter(ch);
|
||||
}
|
||||
|
||||
KeyEvent keyEv{ true, 0, vkey, scanCode, ch, states.Value() };
|
||||
return _terminalInput->HandleKey(&keyEv);
|
||||
// Unfortunately, the UI doesn't give us both a character down and a
|
||||
// character up event, only a character received event. So fake sending both
|
||||
// to the terminal input translator. Unless it's in win32-input-mode, it'll
|
||||
// ignore the keyup.
|
||||
KeyEvent keyDown{ true, 1, vkey, scanCode, ch, states.Value() };
|
||||
KeyEvent keyUp{ false, 1, vkey, scanCode, ch, states.Value() };
|
||||
const auto handledDown = _terminalInput->HandleKey(&keyDown);
|
||||
const auto handledUp = _terminalInput->HandleKey(&keyUp);
|
||||
return handledDown || handledUp;
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
|
@ -764,48 +784,49 @@ void Terminal::_AdjustCursorPosition(const COORD proposedPosition)
|
|||
auto proposedCursorPosition = proposedPosition;
|
||||
auto& cursor = _buffer->GetCursor();
|
||||
const Viewport bufferSize = _buffer->GetSize();
|
||||
bool notifyScroll = false;
|
||||
|
||||
// If we're about to scroll past the bottom of the buffer, instead cycle the
|
||||
// buffer.
|
||||
// GH#5540 - Make sure this is a positive number. We can't create a
|
||||
// negative number of new rows.
|
||||
const auto newRows = std::max(0, proposedCursorPosition.Y - bufferSize.Height() + 1);
|
||||
if (newRows > 0)
|
||||
SHORT rowsPushedOffTopOfBuffer = 0;
|
||||
if (proposedCursorPosition.Y >= bufferSize.Height())
|
||||
{
|
||||
const auto newRows = proposedCursorPosition.Y - bufferSize.Height() + 1;
|
||||
for (auto dy = 0; dy < newRows; dy++)
|
||||
{
|
||||
_buffer->IncrementCircularBuffer();
|
||||
proposedCursorPosition.Y--;
|
||||
rowsPushedOffTopOfBuffer++;
|
||||
}
|
||||
notifyScroll = true;
|
||||
}
|
||||
|
||||
// Update Cursor Position
|
||||
cursor.SetPosition(proposedCursorPosition);
|
||||
|
||||
const COORD cursorPosAfter = cursor.GetPosition();
|
||||
|
||||
// Move the viewport down if the cursor moved below the viewport.
|
||||
if (cursorPosAfter.Y > _mutableViewport.BottomInclusive())
|
||||
bool updatedViewport = false;
|
||||
if (proposedCursorPosition.Y > _mutableViewport.BottomInclusive())
|
||||
{
|
||||
const auto newViewTop = std::max(0, cursorPosAfter.Y - (_mutableViewport.Height() - 1));
|
||||
const auto newViewTop = std::max(0, proposedCursorPosition.Y - (_mutableViewport.Height() - 1));
|
||||
if (newViewTop != _mutableViewport.Top())
|
||||
{
|
||||
_mutableViewport = Viewport::FromDimensions({ 0, gsl::narrow<short>(newViewTop) },
|
||||
_mutableViewport.Dimensions());
|
||||
notifyScroll = true;
|
||||
updatedViewport = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (notifyScroll)
|
||||
if (updatedViewport)
|
||||
{
|
||||
_NotifyScrollEvent();
|
||||
}
|
||||
|
||||
if (rowsPushedOffTopOfBuffer != 0)
|
||||
{
|
||||
// We have to report the delta here because we might have circled the text buffer.
|
||||
// That didn't change the viewport and therefore the TriggerScroll(void)
|
||||
// method can't detect the delta on its own.
|
||||
COORD delta{ 0, -gsl::narrow<SHORT>(newRows) };
|
||||
COORD delta{ 0, -rowsPushedOffTopOfBuffer };
|
||||
_buffer->GetRenderTarget().TriggerScroll(&delta);
|
||||
_NotifyScrollEvent();
|
||||
}
|
||||
|
||||
_NotifyTerminalCursorPositionChanged();
|
||||
|
|
|
@ -36,7 +36,7 @@ namespace TerminalCoreUnitTests
|
|||
class TerminalBufferTests;
|
||||
class TerminalApiTest;
|
||||
class ConptyRoundtripTests;
|
||||
class TerminalAndRendererTests;
|
||||
class ScrollTest;
|
||||
};
|
||||
#endif
|
||||
|
||||
|
@ -103,6 +103,7 @@ public:
|
|||
bool SetDefaultForeground(const COLORREF color) noexcept override;
|
||||
bool SetDefaultBackground(const COLORREF color) noexcept override;
|
||||
|
||||
bool EnableWin32InputMode(const bool win32InputMode) noexcept override;
|
||||
bool SetCursorKeysMode(const bool applicationMode) noexcept override;
|
||||
bool SetKeypadMode(const bool applicationMode) noexcept override;
|
||||
bool EnableVT200MouseMode(const bool enabled) noexcept override;
|
||||
|
@ -117,7 +118,7 @@ public:
|
|||
|
||||
#pragma region ITerminalInput
|
||||
// These methods are defined in Terminal.cpp
|
||||
bool SendKeyEvent(const WORD vkey, const WORD scanCode, const Microsoft::Terminal::Core::ControlKeyStates states) override;
|
||||
bool SendKeyEvent(const WORD vkey, const WORD scanCode, const Microsoft::Terminal::Core::ControlKeyStates states, const bool keyDown) override;
|
||||
bool SendMouseEvent(const COORD viewportPos, const unsigned int uiButton, const ControlKeyStates states, const short wheelDelta) override;
|
||||
bool SendCharEvent(const wchar_t ch, const WORD scanCode, const ControlKeyStates states) override;
|
||||
|
||||
|
@ -204,7 +205,7 @@ private:
|
|||
std::unique_ptr<::Microsoft::Console::VirtualTerminal::StateMachine> _stateMachine;
|
||||
std::unique_ptr<::Microsoft::Console::VirtualTerminal::TerminalInput> _terminalInput;
|
||||
|
||||
std::wstring _title;
|
||||
std::optional<std::wstring> _title;
|
||||
std::wstring _startingTitle;
|
||||
|
||||
std::array<COLORREF, XTERM_COLOR_TABLE_SIZE> _colorTable;
|
||||
|
@ -300,6 +301,6 @@ private:
|
|||
friend class TerminalCoreUnitTests::TerminalBufferTests;
|
||||
friend class TerminalCoreUnitTests::TerminalApiTest;
|
||||
friend class TerminalCoreUnitTests::ConptyRoundtripTests;
|
||||
friend class TerminalCoreUnitTests::TerminalAndRendererTests;
|
||||
friend class TerminalCoreUnitTests::ScrollTest;
|
||||
#endif
|
||||
};
|
||||
|
|
|
@ -409,10 +409,11 @@ CATCH_LOG_RETURN_FALSE()
|
|||
bool Terminal::SetWindowTitle(std::wstring_view title) noexcept
|
||||
try
|
||||
{
|
||||
_title = _suppressApplicationTitle ? _startingTitle : title;
|
||||
|
||||
_pfnTitleChanged(_title);
|
||||
|
||||
if (!_suppressApplicationTitle)
|
||||
{
|
||||
_title.emplace(title);
|
||||
_pfnTitleChanged(_title.value());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
CATCH_LOG_RETURN_FALSE()
|
||||
|
@ -521,6 +522,12 @@ try
|
|||
}
|
||||
CATCH_LOG_RETURN_FALSE()
|
||||
|
||||
bool Terminal::EnableWin32InputMode(const bool win32InputMode) noexcept
|
||||
{
|
||||
_terminalInput->ChangeWin32InputMode(win32InputMode);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Terminal::SetCursorKeysMode(const bool applicationMode) noexcept
|
||||
{
|
||||
_terminalInput->ChangeCursorKeysMode(applicationMode);
|
||||
|
|
|
@ -246,6 +246,19 @@ bool TerminalDispatch::SetCursorKeysMode(const bool applicationMode) noexcept
|
|||
return true;
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - win32-input-mode: Enable sending full input records encoded as a string of
|
||||
// characters to the client application.
|
||||
// Arguments:
|
||||
// - win32InputMode - set to true to enable win32-input-mode, false to disable.
|
||||
// Return Value:
|
||||
// - True if handled successfully. False otherwise.
|
||||
bool TerminalDispatch::EnableWin32InputMode(const bool win32Mode) noexcept
|
||||
{
|
||||
_terminalApi.EnableWin32InputMode(win32Mode);
|
||||
return true;
|
||||
}
|
||||
|
||||
//Routine Description:
|
||||
// Enable VT200 Mouse Mode - Enables/disables the mouse input handler in default tracking mode.
|
||||
//Arguments:
|
||||
|
@ -394,6 +407,9 @@ bool TerminalDispatch::_PrivateModeParamsHelper(const DispatchTypes::PrivateMode
|
|||
case DispatchTypes::PrivateModeParams::ATT610_StartCursorBlink:
|
||||
success = EnableCursorBlinking(enable);
|
||||
break;
|
||||
case DispatchTypes::PrivateModeParams::W32IM_Win32InputMode:
|
||||
success = EnableWin32InputMode(enable);
|
||||
break;
|
||||
default:
|
||||
// If no functions to call, overall dispatch was a failure.
|
||||
success = false;
|
||||
|
|
|
@ -18,6 +18,8 @@ public:
|
|||
bool CursorPosition(const size_t line,
|
||||
const size_t column) noexcept override; // CUP
|
||||
|
||||
bool EnableWin32InputMode(const bool win32InputMode) noexcept override; // win32-input-mode
|
||||
|
||||
bool CursorVisibility(const bool isVisible) noexcept override; // DECTCEM
|
||||
bool EnableCursorBlinking(const bool enable) noexcept override; // ATT610
|
||||
|
||||
|
|
|
@ -181,7 +181,11 @@ void Terminal::SelectNewRegion(const COORD coordStart, const COORD coordEnd)
|
|||
const std::wstring Terminal::GetConsoleTitle() const noexcept
|
||||
try
|
||||
{
|
||||
return _title;
|
||||
if (_title.has_value())
|
||||
{
|
||||
return _title.value();
|
||||
}
|
||||
return _startingTitle;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
|
|
|
@ -52,10 +52,11 @@ namespace Microsoft.Terminal.Settings
|
|||
Windows.UI.Xaml.VerticalAlignment BackgroundImageVerticalAlignment;
|
||||
|
||||
UInt32 SelectionBackground;
|
||||
|
||||
TextAntialiasingMode AntialiasingMode;
|
||||
|
||||
Boolean RetroTerminalEffect;
|
||||
Boolean ForceFullRepaintRendering;
|
||||
Boolean SoftwareRendering;
|
||||
|
||||
TextAntialiasingMode AntialiasingMode;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -33,6 +33,8 @@ namespace Microsoft.Terminal.Settings
|
|||
String StartingTitle;
|
||||
Boolean SuppressApplicationTitle;
|
||||
String WordDelimiters;
|
||||
|
||||
Boolean ForceVTInput;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -93,11 +93,12 @@ namespace winrt::Microsoft::Terminal::Settings::implementation
|
|||
|
||||
GETSET_PROPERTY(ScrollbarState, ScrollState, ScrollbarState::Visible);
|
||||
|
||||
GETSET_PROPERTY(TextAntialiasingMode, AntialiasingMode, TextAntialiasingMode::Grayscale);
|
||||
|
||||
GETSET_PROPERTY(bool, RetroTerminalEffect, false);
|
||||
GETSET_PROPERTY(bool, ForceFullRepaintRendering, false);
|
||||
GETSET_PROPERTY(bool, SoftwareRendering, false);
|
||||
|
||||
GETSET_PROPERTY(TextAntialiasingMode, AntialiasingMode, TextAntialiasingMode::Grayscale);
|
||||
GETSET_PROPERTY(bool, ForceVTInput, false);
|
||||
|
||||
#pragma warning(pop)
|
||||
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
#include "../../renderer/base/Renderer.hpp"
|
||||
#include "../../renderer/vt/Xterm256Engine.hpp"
|
||||
#include "../../renderer/vt/XtermEngine.hpp"
|
||||
#include "../../renderer/vt/WinTelnetEngine.hpp"
|
||||
|
||||
class InputBuffer; // This for some reason needs to be fwd-decl'd
|
||||
#include "../host/inputBuffer.hpp"
|
||||
|
|
|
@ -61,7 +61,8 @@ namespace TerminalCoreUnitTests
|
|||
// Make sure we don't handle Alt+Space. The system will use this to
|
||||
// bring up the system menu for restore, min/maximize, size, move,
|
||||
// close
|
||||
VERIFY_IS_FALSE(term.SendKeyEvent(L' ', 0, ControlKeyStates::LeftAltPressed));
|
||||
VERIFY_IS_FALSE(term.SendKeyEvent(L' ', 0, ControlKeyStates::LeftAltPressed, true));
|
||||
VERIFY_IS_FALSE(term.SendKeyEvent(L' ', 0, ControlKeyStates::LeftAltPressed, false));
|
||||
VERIFY_IS_FALSE(term.SendCharEvent(L' ', 0, ControlKeyStates::LeftAltPressed));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ namespace TerminalCoreUnitTests
|
|||
winrt::hstring StartingTitle() { return _startingTitle; }
|
||||
bool SuppressApplicationTitle() { return _suppressApplicationTitle; }
|
||||
uint32_t SelectionBackground() { return COLOR_WHITE; }
|
||||
bool ForceVTInput() { return false; }
|
||||
|
||||
// other implemented methods
|
||||
uint32_t GetColorTableEntry(int32_t) const { return 123; }
|
||||
|
@ -59,6 +60,7 @@ namespace TerminalCoreUnitTests
|
|||
void StartingTitle(winrt::hstring const& value) { _startingTitle = value; }
|
||||
void SuppressApplicationTitle(bool suppressApplicationTitle) { _suppressApplicationTitle = suppressApplicationTitle; }
|
||||
void SelectionBackground(uint32_t) {}
|
||||
void ForceVTInput(bool) {}
|
||||
|
||||
// other unimplemented methods
|
||||
void SetColorTableEntry(int32_t /* index */, uint32_t /* value */) {}
|
||||
|
|
217
src/cascadia/UnitTests_TerminalCore/ScrollTest.cpp
Normal file
|
@ -0,0 +1,217 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
#include "precomp.h"
|
||||
#include <WexTestClass.h>
|
||||
|
||||
#include <argb.h>
|
||||
#include <DefaultSettings.h>
|
||||
|
||||
#include "../renderer/inc/DummyRenderTarget.hpp"
|
||||
#include "../renderer/base/Renderer.hpp"
|
||||
#include "../renderer/dx/DxRenderer.hpp"
|
||||
|
||||
#include "../cascadia/TerminalCore/Terminal.hpp"
|
||||
#include "MockTermSettings.h"
|
||||
#include "consoletaeftemplates.hpp"
|
||||
#include "TestUtils.h"
|
||||
|
||||
using namespace winrt::Microsoft::Terminal::Settings;
|
||||
using namespace Microsoft::Terminal::Core;
|
||||
using namespace ::Microsoft::Console::Types;
|
||||
|
||||
using namespace WEX::Common;
|
||||
using namespace WEX::Logging;
|
||||
using namespace WEX::TestExecution;
|
||||
|
||||
namespace
|
||||
{
|
||||
class MockScrollRenderTarget final : public ::Microsoft::Console::Render::IRenderTarget
|
||||
{
|
||||
public:
|
||||
~MockScrollRenderTarget() override{};
|
||||
|
||||
std::optional<COORD> TriggerScrollDelta() const
|
||||
{
|
||||
return _triggerScrollDelta;
|
||||
}
|
||||
|
||||
void Reset()
|
||||
{
|
||||
_triggerScrollDelta.reset();
|
||||
}
|
||||
|
||||
virtual void TriggerRedraw(const Microsoft::Console::Types::Viewport&){};
|
||||
virtual void TriggerRedraw(const COORD* const){};
|
||||
virtual void TriggerRedrawCursor(const COORD* const){};
|
||||
virtual void TriggerRedrawAll(){};
|
||||
virtual void TriggerTeardown(){};
|
||||
virtual void TriggerSelection(){};
|
||||
virtual void TriggerScroll(){};
|
||||
virtual void TriggerScroll(const COORD* const delta)
|
||||
{
|
||||
_triggerScrollDelta = { *delta };
|
||||
};
|
||||
virtual void TriggerCircling(){};
|
||||
void TriggerTitleChange(){};
|
||||
|
||||
private:
|
||||
std::optional<COORD> _triggerScrollDelta;
|
||||
};
|
||||
|
||||
struct ScrollBarNotification
|
||||
{
|
||||
int ViewportTop;
|
||||
int ViewportHeight;
|
||||
int BufferHeight;
|
||||
};
|
||||
}
|
||||
|
||||
namespace TerminalCoreUnitTests
|
||||
{
|
||||
class ScrollTest;
|
||||
};
|
||||
using namespace TerminalCoreUnitTests;
|
||||
|
||||
class TerminalCoreUnitTests::ScrollTest final
|
||||
{
|
||||
// !!! DANGER: Many tests in this class expect the Terminal buffer
|
||||
// to be 80x32. If you change these, you'll probably inadvertently break a
|
||||
// bunch of tests !!!
|
||||
static const SHORT TerminalViewWidth = 80;
|
||||
static const SHORT TerminalViewHeight = 32;
|
||||
// For TestNotifyScrolling, it's important that this value is ~=9000.
|
||||
// Something smaller like 100 won't cause the test to fail.
|
||||
static const SHORT TerminalHistoryLength = 9001;
|
||||
|
||||
TEST_CLASS(ScrollTest);
|
||||
|
||||
TEST_METHOD(TestNotifyScrolling);
|
||||
|
||||
TEST_METHOD_SETUP(MethodSetup)
|
||||
{
|
||||
_term = std::make_unique<::Microsoft::Terminal::Core::Terminal>();
|
||||
|
||||
_scrollBarNotification = std::make_shared<std::optional<ScrollBarNotification>>();
|
||||
_term->SetScrollPositionChangedCallback([scrollBarNotification = _scrollBarNotification](const int top, const int height, const int bottom) {
|
||||
ScrollBarNotification tmp;
|
||||
tmp.ViewportTop = top;
|
||||
tmp.ViewportHeight = height;
|
||||
tmp.BufferHeight = bottom;
|
||||
*scrollBarNotification = { tmp };
|
||||
});
|
||||
|
||||
_renderTarget = std::make_unique<MockScrollRenderTarget>();
|
||||
_term->Create({ TerminalViewWidth, TerminalViewHeight }, TerminalHistoryLength, *_renderTarget);
|
||||
return true;
|
||||
}
|
||||
|
||||
TEST_METHOD_CLEANUP(MethodCleanup)
|
||||
{
|
||||
_term = nullptr;
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
std::unique_ptr<Terminal> _term;
|
||||
std::unique_ptr<MockScrollRenderTarget> _renderTarget;
|
||||
std::shared_ptr<std::optional<ScrollBarNotification>> _scrollBarNotification;
|
||||
};
|
||||
|
||||
void ScrollTest::TestNotifyScrolling()
|
||||
{
|
||||
// See https://github.com/microsoft/terminal/pull/5630
|
||||
//
|
||||
// This is a test for GH#5540, in the most bizarre way. The origin of that
|
||||
// bug was that as newlines were emitted, we'd accumulate an enormous scroll
|
||||
// delta into a selection region, to the point of overflowing a SHORT. When
|
||||
// the overflow occurred, the Terminal would fail to send a NotifyScroll() to
|
||||
// the TermControl hosting it.
|
||||
//
|
||||
// For this bug to repro, we need to:
|
||||
// - Have a sufficiently large buffer, because each newline we'll accumulate
|
||||
// a delta of (0, ~bufferHeight), so (bufferHeight^2 + bufferHeight) >
|
||||
// SHRT_MAX
|
||||
// - Have a selection
|
||||
|
||||
Log::Comment(L"Watch out - this test takes a while to run, and won't "
|
||||
L"output anything unless in encounters an error. This is expected.");
|
||||
|
||||
auto& termTb = *_term->_buffer;
|
||||
auto& termSm = *_term->_stateMachine;
|
||||
|
||||
const auto totalBufferSize = termTb.GetSize().Height();
|
||||
|
||||
auto currentRow = 0;
|
||||
|
||||
// We're outputting like 18000 lines of text here, so emitting 18000*4 lines
|
||||
// of output to the console is actually quite unnecessary
|
||||
WEX::TestExecution::SetVerifyOutput settings(WEX::TestExecution::VerifyOutputSettings::LogOnlyFailures);
|
||||
|
||||
// Emit a bunch of newlines to test scrolling.
|
||||
for (; currentRow < totalBufferSize * 2; currentRow++)
|
||||
{
|
||||
*_scrollBarNotification = std::nullopt;
|
||||
_renderTarget->Reset();
|
||||
|
||||
termSm.ProcessString(L"X\r\n");
|
||||
|
||||
// When we're on TerminalViewHeight-1, we'll emit the newline that
|
||||
// causes the first scroll event
|
||||
bool scrolled = currentRow >= TerminalViewHeight - 1;
|
||||
|
||||
// When we circle the buffer, the scroll bar's position does not
|
||||
// change.
|
||||
bool circledBuffer = currentRow >= totalBufferSize - 1;
|
||||
bool expectScrollBarNotification = scrolled && !circledBuffer;
|
||||
|
||||
if (expectScrollBarNotification)
|
||||
{
|
||||
VERIFY_IS_TRUE(_scrollBarNotification->has_value(),
|
||||
fmt::format(L"Expected a 'scroll bar position changed' notification for row {}", currentRow).c_str());
|
||||
}
|
||||
else
|
||||
{
|
||||
VERIFY_IS_FALSE(_scrollBarNotification->has_value(),
|
||||
fmt::format(L"Expected to not see a 'scroll bar position changed' notification for row {}", currentRow).c_str());
|
||||
}
|
||||
|
||||
// If we scrolled but it circled the buffer, then the terminal will
|
||||
// call `TriggerScroll` with a delta to tell the renderer about it.
|
||||
if (scrolled && circledBuffer)
|
||||
{
|
||||
VERIFY_IS_TRUE(_renderTarget->TriggerScrollDelta().has_value(),
|
||||
fmt::format(L"Expected a 'trigger scroll' notification in RenderTarget for row {}", currentRow).c_str());
|
||||
|
||||
COORD expectedDelta;
|
||||
expectedDelta.X = 0;
|
||||
expectedDelta.Y = -1;
|
||||
VERIFY_ARE_EQUAL(expectedDelta, _renderTarget->TriggerScrollDelta().value(), fmt::format(L"Wrong value in 'trigger scroll' notification in RenderTarget for row {}", currentRow).c_str());
|
||||
}
|
||||
else
|
||||
{
|
||||
VERIFY_IS_FALSE(_renderTarget->TriggerScrollDelta().has_value(),
|
||||
fmt::format(L"Expected to not see a 'trigger scroll' notification in RenderTarget for row {}", currentRow).c_str());
|
||||
}
|
||||
|
||||
if (_scrollBarNotification->has_value())
|
||||
{
|
||||
const auto tmp = _scrollBarNotification->value();
|
||||
|
||||
const int expectedTop = std::clamp<int>(currentRow - TerminalViewHeight + 2,
|
||||
0,
|
||||
TerminalHistoryLength);
|
||||
const int expectedHeight = TerminalViewHeight;
|
||||
const int expectedBottom = expectedTop + TerminalViewHeight;
|
||||
if ((tmp.ViewportTop != expectedTop) ||
|
||||
(tmp.ViewportHeight != expectedHeight) ||
|
||||
(tmp.BufferHeight != expectedBottom))
|
||||
{
|
||||
Log::Comment(NoThrowString().Format(L"Expected viewport values did not match on line %d", currentRow));
|
||||
}
|
||||
VERIFY_ARE_EQUAL(tmp.ViewportTop, expectedTop);
|
||||
VERIFY_ARE_EQUAL(tmp.ViewportHeight, expectedHeight);
|
||||
VERIFY_ARE_EQUAL(tmp.BufferHeight, expectedBottom);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,177 +0,0 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
//
|
||||
// This test class is useful for cases where we need a Terminal, a Renderer, and
|
||||
// a DxEngine. Some bugs won't repro without all three actually being hooked up.
|
||||
// Note however, that the DxEngine is not wired up to actually be PaintFrame'd
|
||||
// in this test - it pretty heavily depends on being able to actually get a
|
||||
// render target, and as we're running in a unit test, we don't have one of
|
||||
// those. However, this class is good for testing how invalidation works across
|
||||
// all three.
|
||||
|
||||
#include "precomp.h"
|
||||
#include <WexTestClass.h>
|
||||
|
||||
#include <argb.h>
|
||||
#include <DefaultSettings.h>
|
||||
|
||||
#include "../renderer/inc/DummyRenderTarget.hpp"
|
||||
#include "../renderer/base/Renderer.hpp"
|
||||
#include "../renderer/dx/DxRenderer.hpp"
|
||||
|
||||
#include "../cascadia/TerminalCore/Terminal.hpp"
|
||||
#include "MockTermSettings.h"
|
||||
#include "consoletaeftemplates.hpp"
|
||||
#include "TestUtils.h"
|
||||
|
||||
using namespace winrt::Microsoft::Terminal::Settings;
|
||||
using namespace Microsoft::Terminal::Core;
|
||||
using namespace ::Microsoft::Console::Types;
|
||||
|
||||
using namespace WEX::Common;
|
||||
using namespace WEX::Logging;
|
||||
using namespace WEX::TestExecution;
|
||||
|
||||
namespace TerminalCoreUnitTests
|
||||
{
|
||||
class TerminalAndRendererTests;
|
||||
};
|
||||
using namespace TerminalCoreUnitTests;
|
||||
|
||||
class TerminalCoreUnitTests::TerminalAndRendererTests final
|
||||
{
|
||||
// !!! DANGER: Many tests in this class expect the Terminal buffer
|
||||
// to be 80x32. If you change these, you'll probably inadvertently break a
|
||||
// bunch of tests !!!
|
||||
static const SHORT TerminalViewWidth = 80;
|
||||
static const SHORT TerminalViewHeight = 32;
|
||||
// For TestNotifyScrolling, it's important that this value is ~=9000.
|
||||
// Something smaller like 100 won't cause the test to fail.
|
||||
static const SHORT TerminalHistoryLength = 9001;
|
||||
|
||||
TEST_CLASS(TerminalAndRendererTests);
|
||||
|
||||
TEST_METHOD(TestNotifyScrolling);
|
||||
|
||||
TEST_METHOD_SETUP(MethodSetup)
|
||||
{
|
||||
_term = std::make_unique<::Microsoft::Terminal::Core::Terminal>();
|
||||
|
||||
// Create the renderer
|
||||
_renderer = std::make_unique<::Microsoft::Console::Render::Renderer>(_term.get(), nullptr, 0, nullptr);
|
||||
::Microsoft::Console::Render::IRenderTarget& renderTarget = *_renderer;
|
||||
|
||||
// Create the engine
|
||||
_dxEngine = std::make_unique<::Microsoft::Console::Render::DxEngine>();
|
||||
_renderer->AddRenderEngine(_dxEngine.get());
|
||||
|
||||
// Initialize the renderer, engine for a default font size
|
||||
_renderer->TriggerFontChange(USER_DEFAULT_SCREEN_DPI, _desiredFont, _actualFont);
|
||||
const til::size viewportSize{ TerminalViewWidth, TerminalViewHeight };
|
||||
const til::size fontSize = _actualFont.GetSize();
|
||||
const til::size windowSize = viewportSize * fontSize;
|
||||
VERIFY_SUCCEEDED(_dxEngine->SetWindowSize(windowSize));
|
||||
const auto vp = _dxEngine->GetViewportInCharacters(Viewport::FromDimensions({ 0, 0 }, windowSize));
|
||||
VERIFY_ARE_EQUAL(viewportSize, til::size{ vp.Dimensions() });
|
||||
|
||||
// Set up the Terminal, using the Renderer (which has the engine in it)
|
||||
_term->Create({ TerminalViewWidth, TerminalViewHeight }, TerminalHistoryLength, renderTarget);
|
||||
return true;
|
||||
}
|
||||
|
||||
TEST_METHOD_CLEANUP(MethodCleanup)
|
||||
{
|
||||
_term = nullptr;
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
std::unique_ptr<Terminal> _term;
|
||||
std::unique_ptr<::Microsoft::Console::Render::Renderer> _renderer;
|
||||
std::unique_ptr<::Microsoft::Console::Render::DxEngine> _dxEngine;
|
||||
|
||||
FontInfoDesired _desiredFont{ DEFAULT_FONT_FACE, 0, 10, { 0, DEFAULT_FONT_SIZE }, CP_UTF8 };
|
||||
FontInfo _actualFont{ DEFAULT_FONT_FACE, 0, 10, { 0, DEFAULT_FONT_SIZE }, CP_UTF8, false };
|
||||
};
|
||||
|
||||
void TerminalAndRendererTests::TestNotifyScrolling()
|
||||
{
|
||||
// See https://github.com/microsoft/terminal/pull/5630
|
||||
//
|
||||
// This is a test for GH#5540, in the most bizarre way. The origin of that
|
||||
// bug was that as newlines were emitted, we'd accumulate an enormous scroll
|
||||
// delta into a selection region, to the point of overflowing a SHORT. When
|
||||
// the overflow occurred, the Terminal would fail to send a NotifyScroll() to
|
||||
// the TermControl hosting it.
|
||||
//
|
||||
// For this bug to repro, we need to:
|
||||
// - Have a sufficiently large buffer, because each newline we'll accumulate
|
||||
// a delta of (0, ~bufferHeight), so (bufferHeight^2 + bufferHeight) >
|
||||
// SHRT_MAX
|
||||
// - Have a selection
|
||||
|
||||
Log::Comment(L"Watch out - this test takes a while to run, and won't "
|
||||
L"output anything unless in encounters an error. This is expected.");
|
||||
|
||||
auto& termTb = *_term->_buffer;
|
||||
auto& termSm = *_term->_stateMachine;
|
||||
|
||||
const auto totalBufferSize = termTb.GetSize().Height();
|
||||
|
||||
auto currentRow = 0;
|
||||
bool gotScrollingNotification = false;
|
||||
|
||||
// We're outputting like 18000 lines of text here, so emitting 18000*4 lines
|
||||
// of output to the console is actually quite unnecessary
|
||||
WEX::TestExecution::SetVerifyOutput settings(WEX::TestExecution::VerifyOutputSettings::LogOnlyFailures);
|
||||
|
||||
auto verifyScrolling = [&](const int top, const int height, const int bottom) {
|
||||
const int expectedTop = std::clamp<int>(currentRow - TerminalViewHeight + 2,
|
||||
0,
|
||||
TerminalHistoryLength);
|
||||
|
||||
const int expectedHeight = TerminalViewHeight;
|
||||
const int expectedBottom = expectedTop + TerminalViewHeight;
|
||||
if ((expectedTop != top) ||
|
||||
(expectedHeight != height) ||
|
||||
(expectedBottom != bottom))
|
||||
{
|
||||
Log::Comment(NoThrowString().Format(L"Expected values did not match on line %d", currentRow));
|
||||
}
|
||||
VERIFY_ARE_EQUAL(expectedTop, top);
|
||||
VERIFY_ARE_EQUAL(expectedHeight, height);
|
||||
VERIFY_ARE_EQUAL(expectedBottom, bottom);
|
||||
|
||||
gotScrollingNotification = true;
|
||||
};
|
||||
|
||||
// Hook up the scrolling callback
|
||||
_term->SetScrollPositionChangedCallback(verifyScrolling);
|
||||
|
||||
// Create a selection - the actual bounds don't matter, we just need to have one.
|
||||
_term->SetSelectionAnchor(COORD{ 0, 0 });
|
||||
_term->SetSelectionEnd(COORD{ TerminalViewWidth - 1, 0 });
|
||||
_renderer->TriggerSelection();
|
||||
|
||||
// Emit a bunch of newlines. Eventually, the accumulated scroll delta will
|
||||
// cause an overflow, and cause us to miss a NotifyScroll.
|
||||
for (; currentRow < totalBufferSize * 2; currentRow++)
|
||||
{
|
||||
gotScrollingNotification = false;
|
||||
|
||||
termSm.ProcessString(L"X\r\n");
|
||||
|
||||
// When we're on TerminalViewHeight-1, we'll emit the newline that
|
||||
// causes the first scroll event
|
||||
if (currentRow >= TerminalViewHeight - 1)
|
||||
{
|
||||
VERIFY_IS_TRUE(gotScrollingNotification,
|
||||
fmt::format(L"Expected a scrolling notification for row {}", currentRow).c_str());
|
||||
}
|
||||
else
|
||||
{
|
||||
VERIFY_IS_FALSE(gotScrollingNotification,
|
||||
fmt::format(L"Expected to not see scrolling notification for row {}", currentRow).c_str());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,7 +20,7 @@
|
|||
<ClCompile Include="TerminalApiTest.cpp" />
|
||||
<ClCompile Include="ConptyRoundtripTests.cpp" />
|
||||
<ClCompile Include="TerminalBufferTests.cpp" />
|
||||
<ClCompile Include="TerminalAndRendererTests.cpp" />
|
||||
<ClCompile Include="ScrollTest.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\buffer\out\lib\bufferout.vcxproj">
|
||||
|
|
|
@ -63,11 +63,11 @@ AppHost::~AppHost()
|
|||
_app = nullptr;
|
||||
}
|
||||
|
||||
bool AppHost::OnF7Pressed()
|
||||
bool AppHost::OnDirectKeyEvent(const uint32_t vkey, const bool down)
|
||||
{
|
||||
if (_logic)
|
||||
{
|
||||
return _logic.OnF7Pressed();
|
||||
return _logic.OnDirectKeyEvent(vkey, down);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ public:
|
|||
void AppTitleChanged(const winrt::Windows::Foundation::IInspectable& sender, winrt::hstring newTitle);
|
||||
void LastTabClosed(const winrt::Windows::Foundation::IInspectable& sender, const winrt::TerminalApp::LastTabClosedEventArgs& args);
|
||||
void Initialize();
|
||||
bool OnF7Pressed();
|
||||
bool OnDirectKeyEvent(const uint32_t vkey, const bool down);
|
||||
|
||||
private:
|
||||
bool _useNonClientArea;
|
||||
|
|
|
@ -3,16 +3,11 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include "..\types\IConsoleWindow.hpp"
|
||||
#include "..\types\WindowUiaProviderBase.hpp"
|
||||
|
||||
// Custom window messages
|
||||
#define CM_UPDATE_TITLE (WM_USER)
|
||||
|
||||
#include <wil/resource.h>
|
||||
|
||||
using namespace Microsoft::Console::Types;
|
||||
|
||||
template<typename T>
|
||||
class BaseWindow
|
||||
{
|
||||
|
@ -54,24 +49,8 @@ public:
|
|||
return HandleDpiChange(_window.get(), wparam, lparam);
|
||||
}
|
||||
|
||||
// TODO GitHub #2447: Properly attach WindowUiaProvider for signaling model
|
||||
/*
|
||||
case WM_GETOBJECT:
|
||||
{
|
||||
return HandleGetObject(_window.get(), wparam, lparam);
|
||||
}
|
||||
*/
|
||||
|
||||
case WM_DESTROY:
|
||||
{
|
||||
// TODO GitHub #2447: Properly attach WindowUiaProvider for signaling model
|
||||
/*
|
||||
// signal to uia that they can disconnect our uia provider
|
||||
if (_pUiaProvider)
|
||||
{
|
||||
UiaReturnRawElementProvider(hWnd, 0, 0, NULL);
|
||||
}
|
||||
*/
|
||||
PostQuitMessage(0);
|
||||
return 0;
|
||||
}
|
||||
|
@ -140,22 +119,6 @@ public:
|
|||
return 0;
|
||||
}
|
||||
|
||||
[[nodiscard]] LRESULT HandleGetObject(const HWND hWnd, const WPARAM wParam, const LPARAM lParam)
|
||||
{
|
||||
LRESULT retVal = 0;
|
||||
|
||||
// If we are receiving a request from Microsoft UI Automation framework, then return the basic UIA COM interface.
|
||||
if (static_cast<long>(lParam) == static_cast<long>(UiaRootObjectId))
|
||||
{
|
||||
retVal = UiaReturnRawElementProvider(hWnd, wParam, lParam, _GetUiaProvider());
|
||||
}
|
||||
// Otherwise, return 0. We don't implement MS Active Accessibility (the other framework that calls WM_GETOBJECT).
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
virtual IRawElementProviderSimple* _GetUiaProvider() = 0;
|
||||
|
||||
virtual void OnResize(const UINT width, const UINT height) = 0;
|
||||
virtual void OnMinimize() = 0;
|
||||
virtual void OnRestore() = 0;
|
||||
|
|
|
@ -284,16 +284,6 @@ void IslandWindow::OnSize(const UINT width, const UINT height)
|
|||
{
|
||||
if (_interopWindowHandle != nullptr)
|
||||
{
|
||||
// TODO GitHub #2447: Properly attach WindowUiaProvider for signaling model
|
||||
/*
|
||||
// set the text area to have focus for accessibility consumers
|
||||
if (_pUiaProvider)
|
||||
{
|
||||
LOG_IF_FAILED(_pUiaProvider->SetTextAreaFocus());
|
||||
}
|
||||
break;
|
||||
*/
|
||||
|
||||
// send focus to the child window
|
||||
SetFocus(_interopWindowHandle);
|
||||
return 0; // eat the message
|
||||
|
@ -373,31 +363,6 @@ void IslandWindow::OnSize(const UINT width, const UINT height)
|
|||
return base_type::MessageHandler(message, wparam, lparam);
|
||||
}
|
||||
|
||||
// Routine Description:
|
||||
// - Creates/retrieves a handle to the UI Automation provider COM interfaces
|
||||
// Arguments:
|
||||
// - <none>
|
||||
// Return Value:
|
||||
// - Pointer to UI Automation provider class/interfaces.
|
||||
IRawElementProviderSimple* IslandWindow::_GetUiaProvider()
|
||||
{
|
||||
if (nullptr == _pUiaProvider)
|
||||
{
|
||||
try
|
||||
{
|
||||
// TODO GitHub #3195: Remove WindowUiaProvider in WindowsTerminal
|
||||
//Microsoft::WRL::MakeAndInitialize<WindowUiaProvider>(&_pUiaProvider, this);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
LOG_HR(wil::ResultFromCaughtException());
|
||||
_pUiaProvider = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
return _pUiaProvider;
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Called when the window has been resized (or maximized)
|
||||
// Arguments:
|
||||
|
@ -528,7 +493,7 @@ void IslandWindow::_SetIsFullscreen(const bool fullscreenEnabled)
|
|||
const auto oldIsInFullscreen = _fullscreen;
|
||||
_fullscreen = fullscreenEnabled;
|
||||
|
||||
HWND const hWnd = GetWindowHandle();
|
||||
HWND const hWnd = GetHandle();
|
||||
|
||||
// First, modify regular window styles as appropriate
|
||||
auto windowStyle = GetWindowLongW(hWnd, GWL_STYLE);
|
||||
|
@ -583,7 +548,7 @@ void IslandWindow::_BackupWindowSizes(const bool fCurrentIsInFullscreen)
|
|||
}
|
||||
|
||||
// get and back up the current monitor's size
|
||||
HMONITOR const hCurrentMonitor = MonitorFromWindow(GetWindowHandle(), MONITOR_DEFAULTTONEAREST);
|
||||
HMONITOR const hCurrentMonitor = MonitorFromWindow(GetHandle(), MONITOR_DEFAULTTONEAREST);
|
||||
MONITORINFO currMonitorInfo;
|
||||
currMonitorInfo.cbSize = sizeof(currMonitorInfo);
|
||||
if (GetMonitorInfo(hCurrentMonitor, &currMonitorInfo))
|
||||
|
@ -603,7 +568,7 @@ void IslandWindow::_BackupWindowSizes(const bool fCurrentIsInFullscreen)
|
|||
void IslandWindow::_ApplyWindowSize()
|
||||
{
|
||||
const auto newSize = _fullscreen ? _fullscreenWindowSize : _nonFullscreenWindowSize;
|
||||
LOG_IF_WIN32_BOOL_FALSE(SetWindowPos(GetWindowHandle(),
|
||||
LOG_IF_WIN32_BOOL_FALSE(SetWindowPos(GetHandle(),
|
||||
HWND_TOP,
|
||||
newSize.left,
|
||||
newSize.top,
|
||||
|
|
|
@ -3,15 +3,12 @@
|
|||
|
||||
#include "pch.h"
|
||||
#include "BaseWindow.h"
|
||||
#include "../types/IUiaWindow.h"
|
||||
#include "WindowUiaProvider.hpp"
|
||||
#include <winrt/Microsoft.Terminal.TerminalControl.h>
|
||||
#include <winrt/TerminalApp.h>
|
||||
#include "../../cascadia/inc/cppwinrt_utils.h"
|
||||
|
||||
class IslandWindow :
|
||||
public BaseWindow<IslandWindow>,
|
||||
public IUiaWindow
|
||||
public BaseWindow<IslandWindow>
|
||||
{
|
||||
public:
|
||||
IslandWindow() noexcept;
|
||||
|
@ -22,7 +19,6 @@ public:
|
|||
virtual void OnSize(const UINT width, const UINT height);
|
||||
|
||||
[[nodiscard]] virtual LRESULT MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept override;
|
||||
IRawElementProviderSimple* _GetUiaProvider();
|
||||
void OnResize(const UINT width, const UINT height) override;
|
||||
void OnMinimize() override;
|
||||
void OnRestore() override;
|
||||
|
@ -38,36 +34,6 @@ public:
|
|||
|
||||
void ToggleFullscreen();
|
||||
|
||||
#pragma region IUiaWindow
|
||||
void ChangeViewport(const SMALL_RECT /*NewWindow*/)
|
||||
{
|
||||
// TODO GitHub #1352: Hook up ScreenInfoUiaProvider to WindowUiaProvider
|
||||
// Relevant comment from zadjii-msft:
|
||||
/*
|
||||
In my head for designing this, I'd then have IslandWindow::ChangeViewport
|
||||
call a callback that AppHost sets, where AppHost will then call into the
|
||||
TerminalApp to have TerminalApp handle the ChangeViewport call.
|
||||
(See IslandWindow::SetCreateCallback as an example of a similar
|
||||
pattern we're using today.) That way, if someone else were trying
|
||||
to reuse this, they could have their own AppHost (or TerminalApp
|
||||
equivalent) handle the ChangeViewport call their own way.
|
||||
*/
|
||||
return;
|
||||
};
|
||||
|
||||
HWND GetWindowHandle() const noexcept override
|
||||
{
|
||||
return BaseWindow::GetHandle();
|
||||
};
|
||||
|
||||
[[nodiscard]] HRESULT SignalUia(_In_ EVENTID /*id*/) override { return E_NOTIMPL; };
|
||||
[[nodiscard]] HRESULT UiaSetTextAreaFocus() override { return E_NOTIMPL; };
|
||||
|
||||
RECT GetWindowRect() const noexcept override
|
||||
{
|
||||
return BaseWindow::GetWindowRect();
|
||||
};
|
||||
|
||||
#pragma endregion
|
||||
|
||||
DECLARE_EVENT(DragRegionClicked, _DragRegionClickedHandlers, winrt::delegate<>);
|
||||
|
@ -83,7 +49,6 @@ protected:
|
|||
}
|
||||
|
||||
HWND _interopWindowHandle;
|
||||
WindowUiaProvider* _pUiaProvider;
|
||||
|
||||
winrt::Windows::UI::Xaml::Hosting::DesktopWindowXamlSource _source;
|
||||
|
||||
|
|
|
@ -80,7 +80,7 @@ void NonClientIslandWindow::MakeWindow() noexcept
|
|||
0,
|
||||
0,
|
||||
0,
|
||||
GetWindowHandle(),
|
||||
GetHandle(),
|
||||
nullptr,
|
||||
wil::GetModuleInstanceHandle(),
|
||||
this));
|
||||
|
@ -122,12 +122,13 @@ LRESULT NonClientIslandWindow::_InputSinkMessageHandler(UINT const message, WPAR
|
|||
POINT screenPt{ clientPt };
|
||||
if (ClientToScreen(_dragBarWindow.get(), &screenPt))
|
||||
{
|
||||
auto parentWindow{ GetWindowHandle() };
|
||||
auto parentWindow{ GetHandle() };
|
||||
|
||||
const LPARAM newLparam = MAKELPARAM(screenPt.x, screenPt.y);
|
||||
// Hit test the parent window at the screen coordinates the user clicked in the drag input sink window,
|
||||
// then pass that click through as an NC click in that location.
|
||||
const LRESULT hitTest{ SendMessage(parentWindow, WM_NCHITTEST, 0, MAKELPARAM(screenPt.x, screenPt.y)) };
|
||||
SendMessage(parentWindow, nonClientMessage.value(), hitTest, 0);
|
||||
const LRESULT hitTest{ SendMessage(parentWindow, WM_NCHITTEST, 0, newLparam) };
|
||||
SendMessage(parentWindow, nonClientMessage.value(), hitTest, newLparam);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
@ -402,7 +403,7 @@ int NonClientIslandWindow::_GetResizeHandleHeight() const noexcept
|
|||
// window frame.
|
||||
[[nodiscard]] LRESULT NonClientIslandWindow::_OnNcCalcSize(const WPARAM wParam, const LPARAM lParam) noexcept
|
||||
{
|
||||
if (wParam == false)
|
||||
if (!wParam)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
@ -416,7 +417,7 @@ int NonClientIslandWindow::_GetResizeHandleHeight() const noexcept
|
|||
const auto originalSize = params->rgrc[0];
|
||||
|
||||
// apply the default frame
|
||||
auto ret = DefWindowProc(_window.get(), WM_NCCALCSIZE, wParam, lParam);
|
||||
const auto ret = DefWindowProc(_window.get(), WM_NCCALCSIZE, wParam, lParam);
|
||||
if (ret != 0)
|
||||
{
|
||||
return ret;
|
||||
|
@ -569,7 +570,7 @@ int NonClientIslandWindow::_GetResizeHandleHeight() const noexcept
|
|||
// with that message at the time it was sent to handle the message
|
||||
// correctly.
|
||||
const auto screenPtLparam{ GetMessagePos() };
|
||||
const LRESULT hitTest{ SendMessage(GetWindowHandle(), WM_NCHITTEST, 0, screenPtLparam) };
|
||||
const LRESULT hitTest{ SendMessage(GetHandle(), WM_NCHITTEST, 0, screenPtLparam) };
|
||||
if (hitTest == HTTOP)
|
||||
{
|
||||
// We have to set the vertical resize cursor manually on
|
||||
|
@ -592,7 +593,7 @@ int NonClientIslandWindow::_GetResizeHandleHeight() const noexcept
|
|||
}
|
||||
}
|
||||
|
||||
return DefWindowProc(GetWindowHandle(), WM_SETCURSOR, wParam, lParam);
|
||||
return DefWindowProc(GetHandle(), WM_SETCURSOR, wParam, lParam);
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
|
@ -691,6 +692,14 @@ void NonClientIslandWindow::_UpdateFrameMargins() const noexcept
|
|||
return _OnNcHitTest({ GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) });
|
||||
case WM_PAINT:
|
||||
return _OnPaint();
|
||||
case WM_NCRBUTTONUP:
|
||||
// The `DefWindowProc` function doesn't open the system menu for some
|
||||
// reason so we have to do it ourselves.
|
||||
if (wParam == HTCAPTION)
|
||||
{
|
||||
_OpenSystemMenu(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return IslandWindow::MessageHandler(message, wParam, lParam);
|
||||
|
@ -774,44 +783,20 @@ void NonClientIslandWindow::_UpdateFrameMargins() const noexcept
|
|||
[[nodiscard]] LRESULT NonClientIslandWindow::_OnNcCreate(WPARAM wParam, LPARAM lParam) noexcept
|
||||
{
|
||||
const auto ret = IslandWindow::_OnNcCreate(wParam, lParam);
|
||||
if (ret == FALSE)
|
||||
if (!ret)
|
||||
{
|
||||
return ret;
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// Set the frame's theme before it is rendered (WM_NCPAINT) so that it is
|
||||
// rendered with the correct theme.
|
||||
_UpdateFrameTheme();
|
||||
// This is a hack to make the window borders dark instead of light.
|
||||
// It must be done before WM_NCPAINT so that the borders are rendered with
|
||||
// the correct theme.
|
||||
// For more information, see GH#6620.
|
||||
LOG_IF_FAILED(TerminalTrySetDarkTheme(_window.get(), true));
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Updates the window frame's theme depending on the application theme (light
|
||||
// or dark). This doesn't invalidate the old frame so it will not be
|
||||
// rerendered until the user resizes or focuses/unfocuses the window.
|
||||
// Return Value:
|
||||
// - <none>
|
||||
void NonClientIslandWindow::_UpdateFrameTheme() const
|
||||
{
|
||||
bool isDarkMode;
|
||||
|
||||
switch (_theme)
|
||||
{
|
||||
case ElementTheme::Light:
|
||||
isDarkMode = false;
|
||||
break;
|
||||
case ElementTheme::Dark:
|
||||
isDarkMode = true;
|
||||
break;
|
||||
default:
|
||||
isDarkMode = Application::Current().RequestedTheme() == ApplicationTheme::Dark;
|
||||
break;
|
||||
}
|
||||
|
||||
LOG_IF_FAILED(TerminalTrySetDarkTheme(_window.get(), isDarkMode));
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Called when the app wants to change its theme. We'll update the frame
|
||||
// theme to match the new theme.
|
||||
|
@ -824,7 +809,6 @@ void NonClientIslandWindow::OnApplicationThemeChanged(const ElementTheme& reques
|
|||
IslandWindow::OnApplicationThemeChanged(requestedTheme);
|
||||
|
||||
_theme = requestedTheme;
|
||||
_UpdateFrameTheme();
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
|
@ -862,3 +846,47 @@ bool NonClientIslandWindow::_IsTitlebarVisible() const
|
|||
// updated to include that mode.
|
||||
return !_fullscreen;
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Opens the window's system menu.
|
||||
// - The system menu is the menu that opens when the user presses Alt+Space or
|
||||
// right clicks on the title bar.
|
||||
// - Before updating the menu, we update the buttons like "Maximize" and
|
||||
// "Restore" so that they are grayed out depending on the window's state.
|
||||
// Arguments:
|
||||
// - cursorX: the cursor's X position in screen coordinates
|
||||
// - cursorY: the cursor's Y position in screen coordinates
|
||||
void NonClientIslandWindow::_OpenSystemMenu(const int cursorX, const int cursorY) const noexcept
|
||||
{
|
||||
const auto systemMenu = GetSystemMenu(_window.get(), FALSE);
|
||||
|
||||
WINDOWPLACEMENT placement;
|
||||
if (!GetWindowPlacement(_window.get(), &placement))
|
||||
{
|
||||
return;
|
||||
}
|
||||
const bool isMaximized = placement.showCmd == SW_SHOWMAXIMIZED;
|
||||
|
||||
// Update the options based on window state.
|
||||
MENUITEMINFO mii;
|
||||
mii.cbSize = sizeof(MENUITEMINFO);
|
||||
mii.fMask = MIIM_STATE;
|
||||
mii.fType = MFT_STRING;
|
||||
auto setState = [&](UINT item, bool enabled) {
|
||||
mii.fState = enabled ? MF_ENABLED : MF_DISABLED;
|
||||
SetMenuItemInfo(systemMenu, item, FALSE, &mii);
|
||||
};
|
||||
setState(SC_RESTORE, isMaximized);
|
||||
setState(SC_MOVE, !isMaximized);
|
||||
setState(SC_SIZE, !isMaximized);
|
||||
setState(SC_MINIMIZE, true);
|
||||
setState(SC_MAXIMIZE, !isMaximized);
|
||||
setState(SC_CLOSE, true);
|
||||
SetMenuDefaultItem(systemMenu, UINT_MAX, FALSE);
|
||||
|
||||
const auto ret = TrackPopupMenu(systemMenu, TPM_RETURNCMD, cursorX, cursorY, 0, _window.get(), nullptr);
|
||||
if (ret != 0)
|
||||
{
|
||||
PostMessage(_window.get(), WM_SYSCOMMAND, ret, 0);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -85,5 +85,6 @@ private:
|
|||
void _UpdateFrameMargins() const noexcept;
|
||||
void _UpdateMaximizedState();
|
||||
void _UpdateIslandPosition(const UINT windowWidth, const UINT windowHeight);
|
||||
void _UpdateFrameTheme() const;
|
||||
|
||||
void _OpenSystemMenu(const int mouseX, const int mouseY) const noexcept;
|
||||
};
|
||||
|
|
|
@ -1,159 +0,0 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
#include "pch.h"
|
||||
|
||||
#include "WindowUiaProvider.hpp"
|
||||
|
||||
#include "../host/renderData.hpp"
|
||||
|
||||
HRESULT WindowUiaProvider::RuntimeClassInitialize(Microsoft::Console::Types::IUiaWindow* baseWindow)
|
||||
{
|
||||
return WindowUiaProviderBase::RuntimeClassInitialize(baseWindow);
|
||||
}
|
||||
|
||||
WindowUiaProvider* WindowUiaProvider::Create(Microsoft::Console::Types::IUiaWindow* /*baseWindow*/)
|
||||
{
|
||||
WindowUiaProvider* pWindowProvider = nullptr;
|
||||
//Microsoft::Terminal::TermControlUiaProvider* pScreenInfoProvider = nullptr;
|
||||
try
|
||||
{
|
||||
//pWindowProvider = new WindowUiaProvider(baseWindow);
|
||||
|
||||
// TODO GitHub #2447: Hook up ScreenInfoUiaProvider to WindowUiaProvider
|
||||
// This may be needed for the signaling model
|
||||
/*Globals& g = ServiceLocator::LocateGlobals();
|
||||
CONSOLE_INFORMATION& gci = g.getConsoleInformation();
|
||||
Microsoft::Console::Render::IRenderData* renderData = &gci.renderData;
|
||||
|
||||
pScreenInfoProvider = new Microsoft::Console::Types::ScreenInfoUiaProvider(renderData, pWindowProvider);
|
||||
pWindowProvider->_pScreenInfoProvider = pScreenInfoProvider;
|
||||
*/
|
||||
|
||||
// TODO GitHub #1914: Re-attach Tracing to UIA Tree
|
||||
//Tracing::s_TraceUia(pWindowProvider, ApiCall::Create, nullptr);
|
||||
|
||||
return pWindowProvider;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
if (nullptr != pWindowProvider)
|
||||
{
|
||||
pWindowProvider->Release();
|
||||
}
|
||||
|
||||
LOG_CAUGHT_EXCEPTION();
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] HRESULT WindowUiaProvider::SetTextAreaFocus()
|
||||
{
|
||||
try
|
||||
{
|
||||
// TODO GitHub #2447: Hook up ScreenInfoUiaProvider to WindowUiaProvider
|
||||
// This may be needed for the signaling model
|
||||
//return _pScreenInfoProvider->Signal(UIA_AutomationFocusChangedEventId);
|
||||
return E_NOTIMPL;
|
||||
}
|
||||
CATCH_RETURN();
|
||||
}
|
||||
|
||||
[[nodiscard]] HRESULT WindowUiaProvider::Signal(_In_ EVENTID id)
|
||||
{
|
||||
HRESULT hr = S_OK;
|
||||
|
||||
// ScreenInfoUiaProvider is responsible for signaling selection
|
||||
// changed events and text changed events
|
||||
if (id == UIA_Text_TextSelectionChangedEventId ||
|
||||
id == UIA_Text_TextChangedEventId)
|
||||
{
|
||||
// TODO GitHub #2447: Hook up ScreenInfoUiaProvider to WindowUiaProvider
|
||||
// This may be needed for the signaling model
|
||||
/*if (_pScreenInfoProvider)
|
||||
{
|
||||
hr = _pScreenInfoProvider->Signal(id);
|
||||
}
|
||||
else
|
||||
{
|
||||
hr = E_POINTER;
|
||||
}*/
|
||||
hr = E_POINTER;
|
||||
return hr;
|
||||
}
|
||||
|
||||
if (_signalEventFiring.find(id) != _signalEventFiring.end() &&
|
||||
_signalEventFiring[id] == true)
|
||||
{
|
||||
return hr;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_signalEventFiring[id] = true;
|
||||
}
|
||||
CATCH_RETURN();
|
||||
|
||||
IRawElementProviderSimple* pProvider = static_cast<IRawElementProviderSimple*>(this);
|
||||
hr = UiaRaiseAutomationEvent(pProvider, id);
|
||||
_signalEventFiring[id] = false;
|
||||
|
||||
return hr;
|
||||
}
|
||||
|
||||
#pragma region IRawElementProviderFragment
|
||||
|
||||
IFACEMETHODIMP WindowUiaProvider::Navigate(_In_ NavigateDirection /*direction*/, _COM_Outptr_result_maybenull_ IRawElementProviderFragment** ppProvider)
|
||||
{
|
||||
RETURN_IF_FAILED(_EnsureValidHwnd());
|
||||
*ppProvider = nullptr;
|
||||
HRESULT hr = S_OK;
|
||||
|
||||
// TODO GitHub #2102 or #2447: Hook up ScreenInfoUiaProvider to WindowUiaProvider
|
||||
// This may be needed for the signaling model
|
||||
/*if (direction == NavigateDirection_FirstChild || direction == NavigateDirection_LastChild)
|
||||
{
|
||||
*ppProvider = _pScreenInfoProvider;
|
||||
(*ppProvider)->AddRef();
|
||||
|
||||
// signal that the focus changed
|
||||
LOG_IF_FAILED(_pScreenInfoProvider->Signal(UIA_AutomationFocusChangedEventId));
|
||||
}*/
|
||||
|
||||
// For the other directions (parent, next, previous) the default of nullptr is correct
|
||||
return hr;
|
||||
}
|
||||
|
||||
IFACEMETHODIMP WindowUiaProvider::SetFocus()
|
||||
{
|
||||
RETURN_IF_FAILED(_EnsureValidHwnd());
|
||||
return Signal(UIA_AutomationFocusChangedEventId);
|
||||
}
|
||||
#pragma endregion
|
||||
|
||||
#pragma region IRawElementProviderFragmentRoot
|
||||
|
||||
IFACEMETHODIMP WindowUiaProvider::ElementProviderFromPoint(_In_ double /*x*/,
|
||||
_In_ double /*y*/,
|
||||
_COM_Outptr_result_maybenull_ IRawElementProviderFragment** /*ppProvider*/)
|
||||
{
|
||||
RETURN_IF_FAILED(_EnsureValidHwnd());
|
||||
|
||||
// TODO GitHub #2447: Hook up ScreenInfoUiaProvider to WindowUiaProvider
|
||||
// This may be needed for the signaling model
|
||||
/**ppProvider = _pScreenInfoProvider;
|
||||
(*ppProvider)->AddRef();*/
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
IFACEMETHODIMP WindowUiaProvider::GetFocus(_COM_Outptr_result_maybenull_ IRawElementProviderFragment** /*ppProvider*/)
|
||||
{
|
||||
RETURN_IF_FAILED(_EnsureValidHwnd());
|
||||
// TODO GitHub #2447: Hook up ScreenInfoUiaProvider to WindowUiaProvider
|
||||
// This may be needed for the signaling model
|
||||
//return _pScreenInfoProvider->QueryInterface(IID_PPV_ARGS(ppProvider));
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
#pragma endregion
|
|
@ -1,51 +0,0 @@
|
|||
/*++
|
||||
Copyright (c) Microsoft Corporation
|
||||
Licensed under the MIT license.
|
||||
|
||||
Module Name:
|
||||
- windowUiaProvider.hpp
|
||||
|
||||
Abstract:
|
||||
- This module provides UI Automation access to the console window to
|
||||
support both automation tests and accessibility (screen reading)
|
||||
applications.
|
||||
- Based on examples, sample code, and guidance from
|
||||
https://msdn.microsoft.com/en-us/library/windows/desktop/ee671596(v=vs.85).aspx
|
||||
|
||||
Author(s):
|
||||
- Michael Niksa (MiNiksa) 2017
|
||||
- Austin Diviness (AustDi) 2017
|
||||
- Carlos Zamora (CaZamor) 2019
|
||||
--*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../types/WindowUiaProviderBase.hpp"
|
||||
#include "../types/IUiaWindow.h"
|
||||
|
||||
class WindowUiaProvider final :
|
||||
public Microsoft::Console::Types::WindowUiaProviderBase
|
||||
{
|
||||
public:
|
||||
WindowUiaProvider() = default;
|
||||
HRESULT RuntimeClassInitialize(Microsoft::Console::Types::IUiaWindow* baseWindow);
|
||||
static WindowUiaProvider* Create(Microsoft::Console::Types::IUiaWindow* baseWindow);
|
||||
|
||||
[[nodiscard]] HRESULT Signal(_In_ EVENTID id) override;
|
||||
[[nodiscard]] HRESULT SetTextAreaFocus() override;
|
||||
|
||||
// IRawElementProviderFragment methods
|
||||
IFACEMETHODIMP Navigate(_In_ NavigateDirection direction,
|
||||
_COM_Outptr_result_maybenull_ IRawElementProviderFragment** ppProvider) override;
|
||||
IFACEMETHODIMP SetFocus() override;
|
||||
|
||||
// IRawElementProviderFragmentRoot methods
|
||||
IFACEMETHODIMP ElementProviderFromPoint(_In_ double x,
|
||||
_In_ double y,
|
||||
_COM_Outptr_result_maybenull_ IRawElementProviderFragment** ppProvider) override;
|
||||
IFACEMETHODIMP GetFocus(_COM_Outptr_result_maybenull_ IRawElementProviderFragment** ppProvider) override;
|
||||
|
||||
protected:
|
||||
const OLECHAR* AutomationIdPropertyName = L"Terminal Window";
|
||||
const OLECHAR* ProviderDescriptionPropertyName = L"Microsoft Windows Terminal Window";
|
||||
};
|
|
@ -53,7 +53,6 @@
|
|||
<ClInclude Include="BaseWindow.h" />
|
||||
<ClInclude Include="IslandWindow.h" />
|
||||
<ClInclude Include="NonClientIslandWindow.h" />
|
||||
<ClInclude Include="WindowUiaProvider.hpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="pch.cpp">
|
||||
|
@ -63,7 +62,6 @@
|
|||
<ClCompile Include="AppHost.cpp" />
|
||||
<ClCompile Include="IslandWindow.cpp" />
|
||||
<ClCompile Include="NonClientIslandWindow.cpp" />
|
||||
<ClCompile Include="WindowUiaProvider.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="WindowsTerminal.rc" />
|
||||
|
@ -115,21 +113,6 @@
|
|||
<Error Condition="!Exists('..\..\..\packages\Terminal.ThemeHelpers.0.2.200324001\build\native\Terminal.ThemeHelpers.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\packages\Terminal.ThemeHelpers.0.2.200324001\build\native\Terminal.ThemeHelpers.targets'))" />
|
||||
</Target>
|
||||
|
||||
<!-- **BEGIN VC LIBS HACK** -->
|
||||
<PropertyGroup>
|
||||
<ReasonablePlatform Condition="'$(Platform)'=='Win32'">x86</ReasonablePlatform>
|
||||
<ReasonablePlatform Condition="'$(ReasonablePlatform)'==''">$(Platform)</ReasonablePlatform>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup Condition="'$(WindowsTerminalOfficialBuild)'=='true'">
|
||||
<!-- Add all the CRT libs as content -->
|
||||
<_OpenConsoleVCLibToCopy Include="$(VCToolsRedistInstallDir)\$(ReasonablePlatform)\Microsoft.VC142.CRT\*.dll">
|
||||
<TargetPath>%(Filename)%(Extension)</TargetPath>
|
||||
</_OpenConsoleVCLibToCopy>
|
||||
</ItemGroup>
|
||||
<!-- **END VC LIBS HACK** -->
|
||||
|
||||
|
||||
<!-- Override GetPackagingOutputs to roll up all our dependencies.
|
||||
This ensures that when the WAP packaging project asks what files go into
|
||||
the package, we tell it.
|
||||
|
@ -154,13 +137,25 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackagingOutputs Include="@(_PackagingOutputsFromOtherProjects)" />
|
||||
<!-- (part of the VC LIBS HACK above) -->
|
||||
</ItemGroup>
|
||||
|
||||
<!-- **BEGIN VC LIBS HACK** -->
|
||||
<PropertyGroup>
|
||||
<ReasonablePlatform Condition="'$(Platform)'=='Win32'">x86</ReasonablePlatform>
|
||||
<ReasonablePlatform Condition="'$(ReasonablePlatform)'==''">$(Platform)</ReasonablePlatform>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup Condition="'$(WindowsTerminalOfficialBuild)'=='true'">
|
||||
<!-- Add all the CRT libs as content; these must be inside a Target as they are wildcards. -->
|
||||
<_OpenConsoleVCLibToCopy Include="$(VCToolsRedistInstallDir)\$(ReasonablePlatform)\Microsoft.VC142.CRT\*.dll" />
|
||||
|
||||
<PackagingOutputs Include="@(_OpenConsoleVCLibToCopy)">
|
||||
<ProjectName>$(ProjectName)</ProjectName>
|
||||
<OutputGroup>BuiltProjectOutputGroup</OutputGroup>
|
||||
<TargetPath>%(Filename)%(Extension)</TargetPath>
|
||||
</PackagingOutputs>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- **END VC LIBS HACK** -->
|
||||
</Target>
|
||||
|
||||
<Import Project="$(OpenConsoleDir)\build\rules\GenerateSxsManifestsFromWinmds.targets" />
|
||||
|
|
|
@ -77,6 +77,10 @@ static bool _messageIsF7Keypress(const MSG& message)
|
|||
{
|
||||
return (message.message == WM_KEYDOWN || message.message == WM_SYSKEYDOWN) && message.wParam == VK_F7;
|
||||
}
|
||||
static bool _messageIsAltKeyup(const MSG& message)
|
||||
{
|
||||
return (message.message == WM_KEYUP || message.message == WM_SYSKEYUP) && message.wParam == VK_MENU;
|
||||
}
|
||||
|
||||
int __stdcall wWinMain(HINSTANCE, HINSTANCE, LPWSTR, int)
|
||||
{
|
||||
|
@ -137,13 +141,26 @@ int __stdcall wWinMain(HINSTANCE, HINSTANCE, LPWSTR, int)
|
|||
// been handled we can discard the message before we even translate it.
|
||||
if (_messageIsF7Keypress(message))
|
||||
{
|
||||
if (host.OnF7Pressed())
|
||||
if (host.OnDirectKeyEvent(VK_F7, true))
|
||||
{
|
||||
// The application consumed the F7. Don't let Xaml get it.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// GH#6421 - System XAML will never send an Alt KeyUp event. So, similar
|
||||
// to how we'll steal the F7 KeyDown above, we'll steal the Alt KeyUp
|
||||
// here, and plumb it through.
|
||||
if (_messageIsAltKeyup(message))
|
||||
{
|
||||
// Let's pass <Alt> to the application
|
||||
if (host.OnDirectKeyEvent(VK_MENU, false))
|
||||
{
|
||||
// The application consumed the Alt. Don't let Xaml get it.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
TranslateMessage(&message);
|
||||
DispatchMessage(&message);
|
||||
}
|
||||
|
|
|
@ -123,9 +123,10 @@
|
|||
<!-- Resources -->
|
||||
<!-- This resw only defines things that are used in this package's AppxManifest,
|
||||
so it's not in the common resource items. -->
|
||||
<PRIResource Include="Resources\*\Resources.resw" />
|
||||
<PRIResource Include="Resources\en-US\Resources.resw" />
|
||||
<PRIResource Include="Resources\Resources.resw" />
|
||||
<PRIResource Include="Resources\Resources.devicefamily-core.resw" />
|
||||
<OCResourceDirectory Include="Resources" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- This is picked up by CascadiaResources.build.items. -->
|
||||
|
@ -173,4 +174,6 @@
|
|||
<Error Condition="!Exists('..\..\..\packages\Microsoft.Toolkit.Win32.UI.XamlApplication.6.0.0\build\native\Microsoft.Toolkit.Win32.UI.XamlApplication.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\packages\Microsoft.Toolkit.Win32.UI.XamlApplication.6.0.0\build\native\Microsoft.Toolkit.Win32.UI.XamlApplication.props'))" />
|
||||
<Error Condition="!Exists('..\..\..\packages\Microsoft.Toolkit.Win32.UI.XamlApplication.6.0.0\build\native\Microsoft.Toolkit.Win32.UI.XamlApplication.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\packages\Microsoft.Toolkit.Win32.UI.XamlApplication.6.0.0\build\native\Microsoft.Toolkit.Win32.UI.XamlApplication.targets'))" />
|
||||
</Target>
|
||||
|
||||
<Import Project="$(SolutionDir)build\rules\CollectWildcardResources.targets" />
|
||||
</Project>
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
<PRIResource Include="Resources.devicefamily-core.resw">
|
||||
<Filter>Resources</Filter>
|
||||
</PRIResource>
|
||||
<PRIResource Include="Resources\*\Resources.resw">
|
||||
<PRIResource Include="Resources\en-US\Resources.resw">
|
||||
<Filter>Resources</Filter>
|
||||
</PRIResource>
|
||||
<PRIResource Include="Resources.resw">
|
||||
|
|
|
@ -37,6 +37,9 @@ namespace Microsoft.Terminal.Wpf
|
|||
/// </summary>
|
||||
WM_MOUSEACTIVATE = 0x0021,
|
||||
|
||||
/// <summary>
|
||||
/// The WM_GETOBJECT message is sent by Active Accessibility when a client calls AccessibleObjectFromWindow or any of the other AccessibleObjectFromX APIs that retrieve an interface to an object.
|
||||
/// </summary>
|
||||
WM_GETOBJECT = 0x003D,
|
||||
|
||||
/// <summary>
|
||||
|
@ -49,6 +52,11 @@ namespace Microsoft.Terminal.Wpf
|
|||
/// </summary>
|
||||
WM_KEYDOWN = 0x0100,
|
||||
|
||||
/// <summary>
|
||||
/// The WM_KEYUP message is posted to the window with the keyboard focus when a nonsystem key is released. A nonsystem key is a key that is pressed when the ALT key is not pressed, or a keyboard key that is pressed when a window has the keyboard focus.
|
||||
/// </summary>
|
||||
WM_KEYUP = 0x0101,
|
||||
|
||||
/// <summary>
|
||||
/// The WM_CHAR message is posted to the window with the keyboard focus when a WM_KEYDOWN message is translated by the TranslateMessage function. The WM_CHAR message contains the character code of the key that was pressed.
|
||||
/// </summary>
|
||||
|
@ -207,7 +215,7 @@ namespace Microsoft.Terminal.Wpf
|
|||
public static extern void DestroyTerminal(IntPtr terminal);
|
||||
|
||||
[DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)]
|
||||
public static extern void TerminalSendKeyEvent(IntPtr terminal, ushort vkey, ushort scanCode);
|
||||
public static extern void TerminalSendKeyEvent(IntPtr terminal, ushort vkey, ushort scanCode, bool keyDown);
|
||||
|
||||
[DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)]
|
||||
public static extern void TerminalSendCharEvent(IntPtr terminal, char ch, ushort scanCode);
|
||||
|
|