Compare commits

..

1 Commits

Author SHA1 Message Date
Leonard Hecker
6126532e71 wip 2024-01-15 21:48:31 +01:00
223 changed files with 11617 additions and 7622 deletions

View File

@@ -17,7 +17,6 @@ CMMI
copyable
Counterintuitively
CtrlDToClose
CUI
cybersecurity
dalet
Dcs
@@ -81,13 +80,11 @@ noreply
ogonek
ok'd
overlined
perlw
pipeline
postmodern
Powerline
powerline
ptys
pwshw
qof
qps
rclt
@@ -96,7 +93,6 @@ reserialization
reserialize
reserializes
rlig
rubyw
runtimes
servicebus
shcha
@@ -117,7 +113,6 @@ toolset
truthiness
tshe
ubuntu
UEFI
uiatextrange
UIs
und

View File

@@ -4,7 +4,6 @@ acl
aclapi
alignas
alignof
allocconsolewithoptions
APPLYTOSUBMENUS
appxrecipe
bitfield
@@ -157,7 +156,6 @@ OUTLINETEXTMETRICW
overridable
PACL
PAGESCROLL
PALLOC
PATINVERT
PEXPLICIT
PICKFOLDERS

View File

@@ -20,7 +20,6 @@ cpptools
cppvsdbg
CPRs
cryptbase
cscript
DACL
DACLs
defaultlib
@@ -89,10 +88,8 @@ Virtualization
visualstudio
vscode
VSTHRD
WINBASEAPI
winsdkver
wlk
wscript
wslpath
wtl
wtt

View File

@@ -115,7 +115,6 @@
^src/terminal/parser/ut_parser/Base64Test.cpp$
^src/terminal/parser/ut_parser/run\.bat$
^src/tools/benchcat
^src/tools/ConsoleBench
^src/tools/integrity/dirs$
^src/tools/integrity/packageuwp/ConsoleUWP\.appxSources$
^src/tools/RenderingTests/main\.cpp$

View File

@@ -183,7 +183,6 @@ chh
chshdng
CHT
Cic
CLASSSTRING
CLE
cleartype
CLICKACTIVE
@@ -320,7 +319,6 @@ ctlseqs
CTRLEVENT
CTRLFREQUENCY
CTRLKEYSHORTCUTS
Ctrls
CTRLVOLUME
Ctxt
CUF
@@ -403,7 +401,6 @@ DECECM
DECEKBD
DECERA
DECFI
DECFNK
DECFRA
DECIC
DECID
@@ -429,7 +426,6 @@ DECRQM
DECRQPSR
DECRQSS
DECRQTSR
DECRQUPSS
DECRSPS
decrst
DECSACE
@@ -447,7 +443,6 @@ DECSLPP
DECSLRM
DECSMKR
DECSR
DECST
DECSTBM
DECSTGLT
DECSTR
@@ -486,7 +481,6 @@ directio
DIRECTX
DISABLEDELAYEDEXPANSION
DISABLENOSCROLL
DISPATCHNOTIFY
DISPLAYATTRIBUTE
DISPLAYATTRIBUTEPROPERTY
DISPLAYCHANGE
@@ -1117,8 +1111,8 @@ msix
msrc
MSVCRTD
MTSM
munged
munges
Munged
murmurhash
muxes
myapplet
@@ -1874,12 +1868,7 @@ uiautomationcore
uielem
UIELEMENTENABLEDONLY
UINTs
ul
ulcch
uld
uldb
uldash
ulwave
Unadvise
unattend
UNCPRIORITY
@@ -1902,7 +1891,6 @@ UPDATEDISPLAY
UPDOWN
UPKEY
UPSS
upss
uregex
URegular
usebackq

File diff suppressed because it is too large Load Diff

View File

@@ -43,7 +43,7 @@ jobs:
- template: steps-ensure-nuget-version.yml
- task: NuGetAuthenticate@1
- task: NuGetAuthenticate@0
inputs:
nuGetServiceConnections: 'Terminal Public Artifact Feed'

View File

@@ -78,9 +78,6 @@ extends:
cloudvault: # https://aka.ms/obpipelines/cloudvault
enabled: false
globalSdl: # https://aka.ms/obpipelines/sdl
asyncSdl:
enabled: true
tsaOptionsFile: 'build/config/tsa.json'
tsa:
enabled: true
configFile: '$(Build.SourcesDirectory)\build\config\tsa.json'
@@ -250,7 +247,7 @@ extends:
- stage: Publish
displayName: Publish
dependsOn: [Build]
dependsOn: [Build, Package]
jobs:
- template: ./build/pipelines/templates-v2/job-publish-symbols.yml@self
parameters:

View File

@@ -1,7 +1,7 @@
steps:
- template: steps-ensure-nuget-version.yml
- task: NuGetAuthenticate@1
- task: NuGetAuthenticate@0
- script: |-
echo ##vso[task.setvariable variable=NUGET_RESTORE_MSBUILD_ARGS]/p:Platform=$(BuildPlatform)

View File

@@ -3,9 +3,9 @@
<!-- This file is read by XES, which we use in our Release builds. -->
<PropertyGroup Label="Version">
<XesUseOneStoreVersioning>true</XesUseOneStoreVersioning>
<XesBaseYearForStoreVersion>2024</XesBaseYearForStoreVersion>
<XesBaseYearForStoreVersion>2023</XesBaseYearForStoreVersion>
<VersionMajor>1</VersionMajor>
<VersionMinor>21</VersionMinor>
<VersionMinor>20</VersionMinor>
<VersionInfoProductName>Windows Terminal</VersionInfoProductName>
</PropertyGroup>
</Project>

View File

@@ -9,7 +9,7 @@
<package id="Microsoft.VisualStudio.Setup.Configuration.Native" version="2.3.2262" targetFramework="native" developmentDependency="true" />
<package id="Microsoft.UI.Xaml" version="2.8.4" targetFramework="native" />
<package id="Microsoft.Web.WebView2" version="1.0.1661.34" targetFramework="native" />
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.240122.1" targetFramework="native" developmentDependency="true" />
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.230824.2" targetFramework="native" developmentDependency="true" />
<!-- Managed packages -->
<package id="Appium.WebDriver" version="3.0.0.2" targetFramework="net45" />

View File

@@ -2782,15 +2782,15 @@
"description": "When set to true, marks added to the buffer via the addMark action will appear on the scrollbar.",
"type": "boolean"
},
"experimental.repositionCursorWithMouse": {
"default": false,
"description": "When set to true, you can move the text cursor by clicking with the mouse on the current commandline. This is an experimental feature - there are lots of edge cases where this will not work as expected.",
"type": "boolean"
},
"experimental.pixelShaderPath": {
"description": "Use to set a path to a pixel shader to use with the Terminal. Overrides `experimental.retroTerminalEffect`. This is an experimental feature, and its continued existence is not guaranteed.",
"type": "string"
},
"useAtlasEngine": {
"description": "Windows Terminal 1.16 and later ship with a new, performant text renderer. Set this to false to revert back to the old text renderer.",
"type": "boolean",
"default": true
},
"fontFace": {
"default": "Cascadia Mono",
"description": "[deprecated] Define 'face' within the 'font' object instead.",

View File

@@ -1,396 +0,0 @@
---
author: Dustin Howett @DHowett <duhowett@microsoft.com>
created on: 2020-08-16
last updated: 2023-12-12
issue id: "#7335"
---
# Console Allocation Policy
## Abstract
Due to the design of the console subsystem on Windows as it has existed since Windows 95, every application that is
stamped with the `IMAGE_SUBSYSTEM_WINDOWS_CUI` subsystem in its PE header will be allocated a console by kernel32.
Any application that is stamped `IMAGE_SUBSYSTEM_WINDOWS_GUI` will not automatically be allocated a console.
This has worked fine for many years: when you double-click a console application in your GUI shell, it is allocated a
console. When you run a GUI application from your console shell, it is **not** allocated a console. The shell will
**not** wait for it to exit before returning you to a prompt.
There is a large class of applications that do not fit neatly into this mold. Take Python, Ruby, Perl, Lua, or even
VBScript: These languages are not relegated to running in a console session; they can be used to write fully-fledged GUI
applications like any other language.
Because their interpreters are console subsystem applications, however, any user double-clicking a shortcut to a Python
or Perl application will be presented with a console window that the language runtime may choose to garbage collect, or
may choose not to.
If the runtime chooses to hide the window, there will still be a brief period during which that window is visible. It is
inescapable.
Likewise, any user running that GUI application from a console shell will see their shell hang until the application
terminates.
All of these scripting languages worked around this by shipping two binaries each, identical in every way expect in
their subsystem fields. python/pythonw, perl/perlw, ruby/rubyw, wscript/cscript.
PowerShell[^1] is waiting to deal with this problem because they don't necessarily want to ship a `pwshw.exe` for all
of their GUI-only authors. Every additional `*w` version of an application is an additional maintenance burden and
source of cognitive overhead[^2] for users.
On the other side, you have mostly-GUI applications that want to print output to a console **if there is one
connected**.
These applications are still primarily GUI-driven, but they might support arguments like `/?` or `--help`. They only
need a console when they need to print out some text. Sometimes they'll allocate their own console (which opens a new
window) to display in, and sometimes they'll reattach to the originating console. VSCode does the latter, and so when
you run `code` from CMD, and then `exit` CMD, your console window sticks around because VSCode is still attached to it.
It will never print anything, and your only option is to close it.
There's another risk in reattaching, too. Given that the shell decides whether to wait based on the subsystem
field, GUI subsystem applications that reattach to their owning consoles *just to print some text* end up stomping on
the output of any shell that doesn't wait for them:
```
C:\> application --help
application - the interesting application
C:\> Usage: application [OPTIONS] ...
```
> _(the prompt is interleaved with the output)_
## Solution Design
I propose that we introduce a fusion manifest field, **consoleAllocationPolicy**, with the following values:
* _absent_
* `detached`
This field allows an application to disable the automatic allocation of a console, regardless of the [process creation flags]
passed to [`CreateProcess`] and its subsystem value.
It would look (roughly) like this:
```xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<application>
<windowsSettings>
<consoleAllocationPolicy xmlns="http://schemas.microsoft.com/SMI/2024/WindowsSettings">detached</consoleAllocationPolicy>
</windowsSettings>
</application>
</assembly>
```
The effects of this field will only apply to binaries in the `IMAGE_SUBSYSTEM_WINDOWS_CUI` subsystem, as it pertains to
the particulars of their console allocation.
**All console inheritance will proceed as normal.** Since this field takes effect only in the absence of console
inheritance, CUI applications will still be able to run inside an existing console session.
| policy | behavior |
| - | - |
| _absent_ | _default behavior_ |
| `detached` | The new process is not attached to a console session (similar to `DETACHED_PROCESS`) unless one was inherited. |
An application that specifies the `detached` allocation policy will _not_ present a console window when launched by
Explorer, Task Scheduler, etc.
### Interaction with existing APIs
[`CreateProcess`] supports a number of [process creation flags] that dictate how a spawned application will behave with
regards to console allocation:
* `DETACHED_PROCESS`: No console inheritance, no console host spawned for the new process.
* `CREATE_NEW_CONSOLE`: No console inheritance, new console host **is** spawned for the new process.
* `CREATE_NO_WINDOW`: No console inheritance, new console host **is** spawned for the new process.
* this is the same as `CREATE_NEW_CONSOLE`, except that the first connection packet specifies that the window should
be invisible
Due to the design of [`CreateProcess`] and `ShellExecute`, this specification recommends that an allocation policy of
`detached` _override_ the inclusion of `CREATE_NEW_CONSOLE` in the `dwFlags` parameter to [`CreateProcess`].
> **Note**
> `ShellExecute` passes `CREATE_NEW_CONSOLE` _by default_ on all invocations. This impacts our ability to resolve the
> conflicts between these two APIs--`detached` policy and `CREATE_NEW_CONSOLE`--without auditing every call site in
> every Windows application that calls `ShellExecute` on a console application. Doing so is infeasible.
### Application impact
An application that opts into the `detached` console allocation policy will **not** be allocated a console unless one is
inherited. This presents an issue for applications like PowerShell that do want a console window when they are launched
directly.
Applications in this category can call `AllocConsole()` early in their startup to get fine-grained control over when a
console is presented.
The call to `AllocConsole()` will fail safely if the application has already inherited a console handle. It will succeed
if the application does not currently have a console handle.
> **Note**
> **Backwards Compatibility**: The behavior of `AllocConsole()` is not changing in response to this specification;
> therefore, applications that intend to run on older versions of Windows that do not support console allocation
> policies, which call `AllocConsole()`, will continue to behave normally.
### New APIs
Because a console-subsystem application may still want fine-grained control over when and how its console window is
spawned, we propose the inclusion of a new API, `AllocConsoleWithOptions(PALLOC_CONSOLE_OPTIONS)`.
#### `AllocConsoleWithOptions`
```c++
// Console Allocation Modes
typedef enum ALLOC_CONSOLE_MODE {
ALLOC_CONSOLE_MODE_DEFAULT = 0,
ALLOC_CONSOLE_MODE_NEW_WINDOW = 1,
ALLOC_CONSOLE_MODE_NO_WINDOW = 2
} ALLOC_CONSOLE_MODE;
typedef enum ALLOC_CONSOLE_RESULT {
ALLOC_CONSOLE_RESULT_NO_CONSOLE = 0,
ALLOC_CONSOLE_RESULT_NEW_CONSOLE = 1,
ALLOC_CONSOLE_RESULT_EXISTING_CONSOLE = 2
} ALLOC_CONSOLE_RESULT, *PALLOC_CONSOLE_RESULT;
typedef
struct ALLOC_CONSOLE_OPTIONS
{
ALLOC_CONSOLE_MODE mode;
BOOL useShowWindow;
WORD showWindow;
} ALLOC_CONSOLE_OPTIONS, *PALLOC_CONSOLE_OPTIONS;
WINBASEAPI
HRESULT
WINAPI
AllocConsoleWithOptions(_In_opt_ PALLOC_CONSOLE_OPTIONS allocOptions, _Out_opt_ PALLOC_CONSOLE_RESULT result);
```
**AllocConsoleWithOptions** affords an application control over how and when it begins a console session.
> [!NOTE]
> Unlike `AllocConsole`, `AllocConsoleWithOptions` without a mode (`ALLOC_CONSOLE_MODE_DEFAULT`) will only allocate a console if one was
> requested during `CreateProcess`.
>
> To override this behavior, pass one of `ALLOC_CONSOLE_MODE_NEW_WINDOW` (which is equivalent to being spawned with
> `CREATE_NEW_WINDOW`) or `ALLOC_CONSOLE_MODE_NO_WINDOW` (which is equivalent to being spawned with `CREATE_NO_CONSOLE`.)
##### Parameters
**allocOptions**: A pointer to a `ALLOC_CONSOLE_OPTIONS`.
**result**: An optional out pointer, which will be populated with a member of the `ALLOC_CONSOLE_RESULT` enum.
##### `ALLOC_CONSOLE_OPTIONS`
###### Members
**mode**: See the table below for the descriptions of the available modes.
**useShowWindow**: Specifies whether the value in `showWindow` should be used.
**showWindow**: If `useShowWindow` is set, specifies the ["show command"] used to display your
console window.
###### Return Value
`AllocConsoleWithOptions` will return `S_OK` and populate `result` to indicate whether--and how--a console session was
created.
`AllocConsoleWithOptions` will return a failing `HRESULT` if the request could not be completed.
###### Modes
| Mode | Description |
|:-------------------------------:| ------------------------------------------------------------------------------------------------------------------------------ |
| `ALLOC_CONSOLE_MODE_DEFAULT` | Allocate a console session if (and how) one was requested by the parent process. |
| `ALLOC_CONSOLE_MODE_NEW_WINDOW` | Allocate a console session with a window, even if this process was created with `CREATE_NO_CONSOLE` or `DETACHED_PROCESS`. |
| `ALLOC_CONSOLE_MODE_NO_WINDOW` | Allocate a console session _without_ a window, even if this process was created with `CREATE_NEW_WINDOW` or `DETACHED_PROCESS` |
###### Notes
Applications seeking backwards compatibility are encouraged to delay-load `AllocConsoleWithOptions` or check for its presence in
the `api-ms-win-core-console-l1` APISet.
## Inspiration
Fusion manifest entries are used to make application-scoped decisions like this all the time, like `longPathAware` and
`heapType`.
CUI applications that can spawn a UI (or GUI applications that can print to a console) are commonplace on other
platforms because there is no subsystem differentiation.
## UI/UX Design
There is no UI for this feature.
## Capabilities
### Accessibility
This should have no impact on accessibility.
### Security
One reviewer brought up the potential for a malicious actor to spawn an endless stream of headless daemon processes.
This proposal in no way changes the facilities available to malicious people for causing harm: they could have simply
used `IMAGE_SUBSYSTEM_WINDOWS_GUI` and not presented a UI--an option that has been available to them for 35 years.
### Reliability
This should have no impact on reliability.
### Compatibility
An existing application opting into **detached** may constitute a breaking change, but the scope of the breakage is
restricted to that application and is expected to be managed by the application.
All behavioral changes are opt-in.
> **EXAMPLE**: If Python updates python.exe to specify an allocation policy of **detached**, graphical python applications
> will become double-click runnable from the graphical shell without spawning a console window. _However_, console-based
> python applications will no longer spawn a console window when double-clicked from the graphical shell.
>
> In addition, if python.exe specifies **detached**, Console APIs will fail until a console is allocated.
Python could work around this by calling [`AllocConsole`] or [new API `AllocConsoleWithOptions`](#allocconsolewithoptions)
if it can be detected that console I/O is required.
#### Downlevel
On downlevel versions of Windows that do not understand (or expect) this manifest field, applications will allocate
consoles as specified by their image subsystem (described in the [abstract](#abstract) above).
### Performance, Power, and Efficiency
This should have no impact on performance, power or efficiency.
## Potential Issues
### Shell Hang
I am **not** proposing a change in how shells determine whether to wait for an application before returning to a prompt.
This means that a console subsystem application that intends to primarily present a UI but occasionally print text to a
console (therefore choosing the **detached** allocation policy) will cause the shell to "hang" and wait for it to
exit.
The decision to pause/wait is made entirely in the calling shell, and the console subsystem cannot influence that
decision.
Because the vast majority of shells on Windows "hang" by calling `WaitFor...Object` with a HANDLE to the spawned
process, an application that wants to be a "hybrid" CUI/GUI application will be forced to spawn a separate process to
detach from the shell and then terminate its main process.
This is very similar to the forking model seen in many POSIX-compliant operating systems.
### Launching interactively from Explorer, Task Scheduler, etc.
Applications like PowerShell may wish to retain automatic console allocation, and **detached** would be unsuitable for
them. If PowerShell specifies the `detached` console allocation policy, launching `pwsh.exe` from File Explorer it will
no longer spawn a console. This would almost certainly break PowerShell for all users.
Such applications can use `AllocConsole()` early in their startup.
At the same time, PowerShell wants `-WindowStyle Hidden` to suppress the console _before it's created_.
Applications in this category can use `AllocConsoleWithOptions()` to specify additional information about the new console window.
PowerShell, and any other shell that wishes to maintain interactive launch from the graphical shell, can start in
**detached** mode and then allocate a console as necessary. Therefore:
* PowerShell will set `<consoleAllocationPolicy>detached</consoleAllocationPolicy>`
* On startup, it will process its commandline arguments.
* If `-WindowStyle Hidden` is **not** present (the default case), it can:
* `AllocConsole()` or `AllocConsoleWithOptions(NULL)`
* Either of these APIs will present a console window (or not) based on the flags passed through `STARTUPINFO` during
[`CreateProcess`].
* If `-WindowStyle Hidden` is present, it can:
* `AllocConsoleWithOptions(&alloc)` where `alloc.mode` specifies `ALLOC_CONSOLE_MODE_HIDDEN`
## Future considerations
We're introducing a new manifest field today -- what if we want to introduce more? Should we have a `consoleSettings`
manifest block?
Are there other allocation policies we need to consider?
## Resources
### Rejected Solutions
- A new PE subsystem, `IMAGE_SUBSYSTEM_WINDOWS_HYBRID`
- it would behave like **inheritOnly**
- relies on shells to update and check for this
- checking a subsystem doesn't work right with app execution aliases[^3]
- This is not a new problem, but it digs the hole a little deeper.
- requires standardization outside of Microsoft because the PE format is a dependency of the UEFI specification[^4]
- requires coordination between tooling teams both within and without Microsoft (regarding any tool that operates on
or produces PE files)
- An exported symbol that shells can check for to determine whether to wait for the attached process to exit
- relies on shells to update and check for this
- cracking an executable to look for symbols is probably the last thing shells want to do
- we could provide an API to determine whether to wait or return?
- fragile, somewhat silly, exporting symbols from EXEs is annoying and uncommon
An earlier version of this specification offered the **always** allocation policy, with the following behaviors:
> **STRUCK FROM SPECIFICATION**
>
> * A GUI subsystem application would always get a console window.
> * A command-line shell would not wait for it to exit before returning a prompt.
It was cut because a GUI application that wants a console window can simply attach to an existing console session or
allocate a new one. We found no compelling use case that would require the forced allocation of a console session
outside of the application's code.
An earlier version of this specification offered the **inheritOnly** allocation policy, instead of the finer-grained
**hidden** and **detached** policies. We deemed it insufficient for PowerShell's use case because any application
launched by an **inheritOnly** PowerShell would immediately force the uncontrolled allocation of a console window.
> **STRUCK FROM SPECIFICATION**
>
> The move to **hidden** allows PowerShell to offer a fully-fledged console connection that can be itself inherited by a
> downstream application.
#### Additional allocation policies
An earlier revision of this specification suggested two allocation policies:
> **STRUCK FROM SPECIFICATION**
>
> **hidden** is intended to be used by console applications that want finer-grained control over the visibility of their
> console windows, but that still need a console host to service console APIs. This includes most scripting language
> interpreters.
>
> **detached** is intended to be used by primarily graphical applications that would like to operate against a console _if
> one is present_ but do not mind its absence. This includes any graphical tool with a `--help` or `/?` argument.
The `hidden` policy was rejected due to an incompatibility with modern console hosting, as `hidden` would require an
application to interact with the console window via `GetConsoleWindow()` and explicitly show it.
> **STRUCK FROM SPECIFICATION**
>
> ##### ShowWindow and ConPTY
>
> The pseudoconsole creates a hidden window to service `GetConsoleWindow()`, and it can be trivially shown using
> `ShowWindow`. If we recommend that applications `ShowWindow` on startup, we will need to guard the pseudoconsole's
> pseudo-window from being shown.
[^1]: [Powershell -WindowStyle Hidden still shows a window briefly]
[^2]: [StackOverflow: pythonw.exe or python.exe?]
[^3]: [PowerShell: Windows Store applications incorrectly assumed to be console applications]
[^4]: [UEFI spec 2.6 appendix Q.1]
[Powershell -WindowStyle Hidden still shows a window briefly]: https://github.com/PowerShell/PowerShell/issues/3028
[PowerShell: Windows Store applications incorrectly assumed to be console applications]: https://github.com/PowerShell/PowerShell/issues/9970
[StackOverflow: pythonw.exe or python.exe?]: https://stackoverflow.com/questions/9705982/pythonw-exe-or-python-exe
[UEFI spec 2.6 appendix Q.1]: https://www.uefi.org/sites/default/files/resources/UEFI%20Spec%202_6.pdf
[`AllocConsole`]: https://docs.microsoft.com/windows/console/allocconsole
[`CreateProcess`]: https://docs.microsoft.com/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw
[process creation flags]: https://docs.microsoft.com/en-us/windows/win32/procthread/process-creation-flags
["show command"]: https://learn.microsoft.com/windows/win32/api/winuser/nf-winuser-showwindow

View File

@@ -1,101 +0,0 @@
---
author: Mike Griese
created on: 2023-01-26
last updated: 2023-01-26
issue id: n/a
---
# Windows Terminal Copilot | Explain that
## Abstract
## Background
### Inspiration
### Execution Strategy
### User Stories
### Elevator Pitch
It's copilot. For the command line.
## Business Justification
## Scenario Details
### UI/UX Design
### Implementation Details
## Tenents
<table>
<tr><td><strong>Compatibility</strong></td><td>
[comment]: # Will the proposed change break existing code/behaviors? If so, how, and is the breaking change "worth it"?
</td></tr>
<tr><td><strong>Accessibility</strong></td><td>
[comment]: # TODO!
</td></tr>
<tr><td><strong>Sustainability</strong></td><td>
[comment]: # TODO!
</td></tr>
<tr><td><strong>Compatibility</strong></td><td>
[comment]: # TODO!
</td></tr>
<tr><td><strong>Localization</strong></td><td>
[comment]: # TODO!
</td></tr>
</table>
[comment]: # If there are any other potential issues, make sure to include them here.
## To-do list
### 🐣 Crawl
* [ ]
### 🚶 Walk
* [ ]
### 🏃‍♂️ Run
* [ ]
### 🚀 Sprint
* [ ]
## Conclusion
[comment]: # Of the above proposals, which should we decide on, and why?
### Future Considerations
[comment]: # Are there other future features planned that might affect the current design of this setting? The team can help with this section during the review.
## Resources
### Footnotes
<a name="footnote-1"><a>[1]:
[Fig]: https://github.com/withfig/autocomplete
[Warp]: https://www.warp.dev/
[Terminal North Star]: ../Terminal-North-Star.md
[Tasks]: ../Tasks.md
[Shell Integration]: ../Shell-Integration-Marks.md
[Suggestions UI]: ../Suggestions-UI.md
[Extensions]: ../Suggestions-UI.md
<!-- TODO! -->
[shell-driven autocompletion]: ../Terminal-North-Star.md#Shell_autocompletion

View File

@@ -1,102 +0,0 @@
---
author: Mike Griese
created on: 2023-01-26
last updated: 2023-01-26
issue id: n/a
---
# Windows Terminal Copilot | Implicit Suggestions
## Abstract
## Background
### Inspiration
### Execution Strategy
### User Stories
### Elevator Pitch
It's copilot. For the command line.
## Business Justification
## Scenario Details
### UI/UX Design
### Implementation Details
## Tenents
<table>
<tr><td><strong>Compatibility</strong></td><td>
[comment]: # Will the proposed change break existing code/behaviors? If so, how, and is the breaking change "worth it"?
</td></tr>
<tr><td><strong>Accessibility</strong></td><td>
[comment]: # TODO!
</td></tr>
<tr><td><strong>Sustainability</strong></td><td>
[comment]: # TODO!
</td></tr>
<tr><td><strong>Compatibility</strong></td><td>
[comment]: # TODO!
</td></tr>
<tr><td><strong>Localization</strong></td><td>
[comment]: # TODO!
</td></tr>
</table>
[comment]: # If there are any other potential issues, make sure to include them here.
## To-do list
### 🐣 Crawl
* [ ]
### 🚶 Walk
* [ ]
### 🏃‍♂️ Run
* [ ]
### 🚀 Sprint
* [ ]
## Conclusion
[comment]: # Of the above proposals, which should we decide on, and why?
### Future Considerations
[comment]: # Are there other future features planned that might affect the current design of this setting? The team can help with this section during the review.
## Resources
### Footnotes
<a name="footnote-1"><a>[1]:
[Fig]: https://github.com/withfig/autocomplete
[Warp]: https://www.warp.dev/
[Terminal North Star]: ../Terminal-North-Star.md
[Tasks]: ../Tasks.md
[Shell Integration]: ../Shell-Integration-Marks.md
[Suggestions UI]: ../Suggestions-UI.md
[Extensions]: ../Suggestions-UI.md
<!-- TODO! -->
[shell-driven autocompletion]: ../Terminal-North-Star.md#Shell_autocompletion

View File

@@ -1,284 +0,0 @@
---
author: Mike Griese
created on: 2023-01-26
last updated: 2023-01-26
issue id: n/a
---
# Windows Terminal Copilot | Overview
## Abstract
GitHub Copilot is a fairly revolutionary tool that offers complex predictions
for code from the context of the file you're working on and some simple
comments. However, there's more potential to use it outside of just the text
editor. Imagine integration directly with the commandline, where Copilot can
offer suggestions based off of descriptions of what you'd like to do. Recent
advances in AI models can enable dramatic new features like this, which can be
added to the Terminal.
## Background
Imagine Copilot turning "get the process using the most CPU" into `Get-Process |
Sort-Object CPU -Desc | Select-Object ID, Name, CPU -First 1`. Both [Fig] and
[Warp] have produced similar compelling user experiences already, powered by AI.
Github Labs are also working on a similar natural language-to-command model with
[Copilot CLI].
Or imagine suggestions based off your command history itself - I just ran `git
add --all`, and Copilot can suggest `git commit ; git push ; gh pr create`. It
remains an open question if existing AI models are capable of predicting
commands based on what the user has previously done at the command line. If it
isn't yet possible, then undoubtably it will be possible soon. This is an
idealized future vision for AI in the Terminal. Imagine "**Intelli**sense for
the commandline, powered by artificial **intelligence**"
Another scenario that current models excel at is explaining code in natural
human language. The commandline is an experience that's frequently filled with
esoteric commands and error messages that might be unintuitive. Imagine if the
Terminal could automatically provide an explanation for error messages right in
the context of the Terminal itself. No need to copy the message, leave what
you're doing and search the web to find an explanation - the answer is right
there.
### Execution Strategy
Executing on this vision will require a careful hand. As much delight as this
feature might bring, it has equal potential for PR backlash. Developers already
hate the concept of "telemetry" on Windows. The idea that the Windows Terminal
has built-in support for logging _every command_ run on the command line, and
sending it to a Microsoft server is absolutely a recipe for a PR nightmare.
Under no circumstances should this be built directly in to the Terminal.
This doc outlines how the Terminal might enable this functionality via a "GitHub
Copilot Extension". Instead of building Copilot straight into the Terminal, it
would become an optional extension users could install. By making this
explicitly a "GitHub Copilot" branded extension, it's clear to the users how the
extension is maintained and operated - it's not a feature of _Windows_, but
instead a _GitHub_ feature.
### User Stories
When it regards Copilot integration in the Terminal, we're considering on the following four scenarios.
1. **[Prompting]**: The User types a prompt, and the AI suggests some commands given that prompt
- For example, the user literally types "give me a list of all processes with
port 12345 open", and that prompt is sent to the AI model to generate
suggestions.
2. **[Implicit Suggestions]**: A more seamless suggestion based solely on what the user has already typed
- In this scenario, the user can press a keybinding to summon the AI to
suggest a command based solely on the contents of the buffer.
- This version will more heavily rely on [Shell Integration]
- This will be referred to as **"Implicit suggestions"**
3. **"[Explain that]"**: Highlight some command, and ask Copilot to explain what it does.
- Additionally, a quick icon that appears when a command fails, to ask AI to
try and explain what an error means.
4. Long lived context - the AI learns over time from your own patterns, and
makes personalized suggestions.
For the sake of this document, we're going to focus on the first three
experiences. The last, while an interesting future idea, is not something we
have the engineering resources to build. We can leverage existing AI models for
the first three in all likelihood.
Each of the first three scenarios is broken down in greater detail in their linked docs.
The following plan refers to specifically overarching elements of the Copilot
extension, which are the same regardless of individual features of the
extension. This list was made with consideration for what's possible _before
Build 2023_, alongside what we want to do _in the fullness of time_.
#### By Build
Story | Size | Description
--|-----------|--
A | 🐣 Crawl | The Terminal can use a authentication token hardcoded in their settings for OpenAI requests
A | 🚶 Walk | The Terminal can load the user's GitHub identity from Windows
#### After Build
Story | Size | Description
--|-----------|--
A | 🐣 Crawl | The Terminal can load in-proc extensions via Dynamic Dependencies
A | 🚶 Walk | Terminal Extensions can provide their own action handlers
A | 🚶 Walk | Terminal Extensions can query the contents of the text buffer
A | 🚶 Walk | [Shell integration] marks can be used to help make AI suggestions more context-relevant
A | 🏃‍♂️ Run | Extensions can provide their own UI elements into the Terminal
A | 🏃‍♂️ Run | Copilot is delivered as an extension to the Terminal
A | 🚀 Sprint | The Terminal supports a status bar that shows the state of the Copilot extension
> **Warning**: TODO! How much of this spec should be the "extensions" spec, vs the
> "copilot" spec? Most of the "work" described by this spec is just "Make
> extensions work". Might want to flesh out that one then.
#### North star user experience
As the user is typing at the commandline, suggestions appear as they type, with
AI-driven suggestions for what to complete. These suggestions are driven by the
context of the commands they've previously run (and possibly other contents of
the buffer).
The user can highlight parts of a command that they don't understand, and have
the command explained in natural language. Commands that result in errors can
provide a menu for explaining what the error is, and how to remedy the issue.
### Elevator Pitch
It's Copilot. For the command line.
## Business Justification
It will delight developers.
## Scenario Details
"AI in the Terminal" covers a number of features each powered by AI. Each of
those features is broken into their own specs (linked above). Please refer to
those docs for details about each individual scenario.
This doc will largely focus on the overarching goal of "how do we deliver
Copilot in the Terminal?".
### Implementation Details
#### Github Authentication
<sup>_By Build 2023_</sup>
We don't know if this will be powered by Github Copilot, or some other
authentication method. This section is left blank while we await those answers.
> **Warning**: TODO! do this
#### Extensions implementation
<sup>_After Build 2023_</sup>
> **Warning**: TODO! do this
Extensions for the Terminal are possible made possible by [Dynamic Dependencies for Main packages]. This is a new feature in Windows SV2 (build 22533 I believe). This enables the Terminal to "pin" another application to the Terminal's own package graph, and load binaries from that package.
Main Packages can declare themselves with the following:
```xml
<Package>
<Properties>
<uap15:dependencyTarget>true</uap15:dependencyTarget>
</Properties>
</Package>
```
This is a new property in the SV2 SDK. That'll allow them be a target of a
Dynamic Dependency. This means that **extensions will be limited to users
running SV2+ builds of Windows**.
```xml
<Package>
<Properties>
<uap15:dependencyTarget>true</uap15:dependencyTarget>
</Properties>
<Applications>
<Application Id="App"
Executable="$targetnametoken$.exe"
EntryPoint="$targetentrypoint$">
<Extensions>
<uap3:Extension Category="windows.appExtension">
<uap3:AppExtension Name="com.microsoft.windows.terminal.extension"
Id="MyTerminalExtension"
DisplayName="...">
<uap3:Properties>
<!-- TODO! Determine what properties we want to put in here -->
<Clsid>{2EACA947-FFFF-4CFA-BA87-BE3FB3EF83EF}</Clsid>
</uap3:Properties>
</uap3:AppExtension>
</uap3:Extension>
</Extensions>
</Application>
</Applications>
</Package>
```
#### Consuming extensions from the Terminal
<sup>_After Build 2023_</sup>
> **Warning**: TODO! do this
## Tenents & Potential Issues
See the individual docs for compatibility, accessibility, and localization
concerns relevant to each feature.
## To-do list
> **Note**: Refer to the individual docs for more detailed plans specific to
> each feature. This section is dedicated to covering only the broad tasks that
> are relevant to the Copilot extension as a whole.
## Before Build Todo's
### 🐣 Crawl
* [ ] Allow the user to store their OpenAI API key in the `settings.json`,
which we'll use for authentication
* This is just a placeholder task for the sake of prototyping, until a real
authentication method is settled on.
### 🚶 Walk
* [ ] Actually do proper authentication.
* This might be through a Github device flow, or a DevID login.
* Remove the support for just pasting the API key in `settings.json` at this point.
## After Build Todo's
> **Warning**: TODO! Almost everything here is just "enable extensions". That might deserve a separate spec.
### 🐣 Crawl
* [ ]
### 🚶 Walk
* [ ]
### 🏃‍♂️ Run
* [ ]
### 🚀 Sprint
* [ ]
## Conclusion
### Future Considerations
#### Shell-driven AI
This document focuses mainly on Terminal-side AI features. We are not precluding
the possiblity that an individual shell may want to implement AI-driven
suggestions as well. Consider PowerShell - they may want to deliver AI powered
suggestions as a completion handler. We will want to provide ways of helping to
promote their experience, rather than focus on a single implementation.
The best way for us to help elevate their experience would be through the
[Suggestions UI] and [shell-driven autocompletion]. This will allow us to
promote their results to a first-class UI control. This is a place where we can
work together better, rather than trying to pick one singular design in this
space and discarding the others.
Similarly, [Copilot CLI] could deliver their results as [shell-driven
autocompletion], to further elevate the experience in the terminal.
## Resources
### Footnotes
<a name="footnote-1"><a>[1]:
[Fig]: https://fig.io/user-manual/ai
[Warp]: https://docs.warp.dev/features/entry/ai-command-search
[Copilot CLI]: https://githubnext.com/projects/copilot-cli/
[Terminal North Star]: ../Terminal-North-Star.md
[Tasks]: ../Tasks.md
[Shell Integration]: ../Shell-Integration-Marks.md
[Suggestions UI]: ../Suggestions-UI.md
[Extensions]: ../Suggestions-UI.md
[Implicit Suggestions]: ./Implicit-Suggestions.md
[Prompting]: ./Prompting.md
[Explain that]: ./Explain-that.md
<!-- TODO! -->
[shell-driven autocompletion]: ../Terminal-North-Star.md#Shell_autocompletion
[Dynamic Dependencies for Main packages]: TODO!

View File

@@ -1,320 +0,0 @@
---
author: Mike Griese
created on: 2023-01-26
last updated: 2023-01-27
issue id: n/a
---
# Windows Terminal Copilot | Prompting
## Abstract
GitHub Copilot is a fairly revolutionary tool that offers complex predictions
for code from the context of the file you're working on and some simple
comments. We envision a scenario where this AI model can be integrated directly
within the Terminal application. This would enable users to type a natural
language description of what they're hoping to do, and recieve suggested
commands to accomplish that task. This has the potential to remove the need for
commandline users to memorize long sets of esoteric flags and options for
commands. Instead, they can simply describe what they want done, and _do it_.
This is one of the many scenarios being considered under the umbrella of "AI in the Terminal". For the other scenarios, see [Overview].
## Background
### Inspiration
Github's own Copilot service was what sparked the initial interest in this area.
This quickly lead to the thought "If it can do this for code, can it work for
command lines too?".
This likely started a cascade of similar implementations across the command-line
ecosystem. Both [Fig] and [Warp] have produced similar compelling user
experiences already, powered by AI. Github Labs are also working on a similar
natural language-to-command model with [Copilot CLI].
This seems to be one of the scenarios that can generate the most value quickly
with existing AI models, which is why it's generated so much interest.
### User Stories
The following plan was made with consideration for what's possible _before Build 2023_, alongside what we want to do _in the fullness of time_.
#### By Build
Story | Size | Description
--|-----------|--
A | ✅ Done | The user can "disable" the extension (by unbinding the action)
A | 🐣 Crawl | The user can use an action to open a dedicated "AI Palette" for prompt-driven AI suggestions.
A | 🐣 Crawl | Suggested results appear as text in the Terminal Control, before the user accepts the command
A | 🐣 Crawl | The AI palette can use a manual API key in the settings to enable openAI access
A | 🚶 Walk | The AI Palette uses an official authentication method (Github login, DevID, etc.)
A | 🚶 Walk | The AI Palette remembers previous queries, for quick recollection and modification.
A | 🚶 Walk | The AI Palette informs the user if they're not authenticated to use the extension
#### After Build
Story | Size | Description
--|-----------|--
A | 🚶 Walk | The AI palette is delivered as an extension to the Terminal
A | 🏃‍♂️ Run | The AI Palette can be moved, resized while hovering
A | 🏃‍♂️ Run | The AI Palette can be docked from a hovering control to a Pane
### Elevator Pitch
It's Copilot. For the command line.
## Business Justification
It will delight developers.
## Scenario Details
### UI/UX Design
![A VERY rough mockup of what this UI might look like](./img/Copilot-in-cmdpal.png)
> **Warning**: TODO! Get mocks from Rodney
### Implementation Details
We'll add a new Control to the Terminal, which we'll dub the `AiPalette`. This
will be forked from the `CommandPalette` code initially, but not built directly
in to it. This `AiPalette` will have a text box, and should be capable of "previewing" actions, in the same way that the Command Palette is. The only action it should need to preview is `sendInput` (which has a prototype implementation linked to [#12861]).
We'll add a new action to invoke this `AiPalette`, which we'll temporarily call
`experimental.ai.prompt`. This will work vaguely like the `commandPalette`
action.
Considering the UX pattern for the OpenAI models is largely conversational, it
will be helpful to users to have a history of the requests they've made, and the
results the model returned, in the UI somewhere. We can store these previous
commands and results in an array in `state.json`. This would work similar to the
way the Command Palette's Commandline mode works currently. We'll need to make a
small modification to store and array of `{prompt, result}` objects, but that
should be fairly trivial.
#### Authentication
<sup>_By Build 2023_</sup>
We don't know if this will be powered by Github Copilot, or some other
authentication method.
While we sort that out, we'll need to make engineering progress, regardless. To
facilitate that, we should just add temporary support for a user to paste an
OpenAI API key in the `settings.json`. This should be good enough to get us
unblocked and making progress with at least one AI model, which we sort out the
specifics of authentication and the backend.
> **Warning**: TODO! Figure out what the official plan here will be, and do that.
#### `HoverPane`
<sup>_By Build 2023_</sup>
After the initial implementation of the `AiPalette`, we'll want to refactor the code slightly to enable arbitrary content to float above the Terminal. This would provide a consistent UI experience for transient content.
This would be something like a `HoverPane` control, which accepts a
`FrameworkElement` as the `Content` property. We'd extract out the actual list
view, text box, etc. of the `AiPalette` and instead invoke a new `HoverPane`
with that `AiPalette` as the content.
This we want to do _before_ Build. This same `HoverPane` could be used to
support **[Explain that]**. That's another scenario we'd like demo'd by Build,
so being able to re-use the same UI base would make sense.
This would also make it easy to swap out the `Content` of the `HoverPane` to
replace it with whatever we need to support authentication flows.
> **Warning**: TODO! Refine this idea after we get mocks from design.
#### Pinning a `HoverPane` to a first-class `Pane`
<sup>_After Build 2023_</sup>
This will require us to support non-terminal content in `Pane`s ([#977]). `Pane`
as a class if very special cased for hosting a `TermControl`, and hosting other
types of `FrameworkElement`s is something that will take some refactoring to
enable. For more details, refer to the separate spec detailing [non-terminal panes](https://github.com/microsoft/terminal/blob/main/doc/specs/drafts/%23997%20Non-Terminal-Panes.md).
Pinning the `HoverPane` would create a new pane, split from the currently active pane.
> **Warning**: TODO! Refine this idea after we get mocks from design.
#### Moving and resizing the `HoverPane`
<sup>_After Build 2023_</sup>
> **Warning**: TODO! after build.
#### Send feedback on the quality of the suggestions
<sup>_After Build 2023_</sup>
> **Warning**: TODO! after build.
## Tenents
<table>
<tr><td><strong>Compatibility</strong></td><td>
We don't expect any regressions while implementing these new features.
</td></tr>
<tr><td><strong>Accessibility</strong></td><td>
Largely, we expect the `AiPalette` to follow the same UIA patterns laid out by the Command Palette before it.
</td></tr>
<tr><td><strong>Localization</strong></td><td>
This feature might end up making the Terminal _more_ accessible to users who's
primary language is not English. The commandline is a fairly ascii-centric
experience in general. It might be a huge game changer for users from
less-represented languages to be able to describe in their native language what
they want to do. They wouldn't need to parse search results from the web that
might not be in their native language. The AI model would do that for them.
</td></tr>
</table>
[comment]: # If there are any other potential issues, make sure to include them here.
## To-do list
## Before Build Todo's
### 🐣 Crawl
* [ ] Introduce a new `AiPalette` control, initially forked from the
`CommandPalette` code
* [ ] TODO! We need design comps to really know what to build here.
* [ ] For the initial commit, just have it accept a prompt and generate a fake
/ placeholder "response"
* [ ] Add a placeholder `experimental.ai.prompt` `ShortcutAction` to open that
`AiPalette`. Bind to no key by default.
* [ ] Make `sendInput` actions previewable, so the text will appear in the
`TermControl` as a _preview_.
* [ ] Hook up an AI model to it. Doesn't have to be the real, final one. Just
_an_ AI model.
* [ ]
### 🚶 Walk
* [ ] Stash the queries (and responses?) in `state.json`, so that we can bring
them back immediately (like the Commandline Mode of the CommandPalette)
* [ ] Move the content of the `AiPalette` into one control, that's hosted by a
`HoverPane` control
* this would be to allow **[Explain that]** to reuse the `HoverPane`.
* This can easily be moved to post-Build if we don't intend to demo [Explain
that] at Build.
* [ ] If the user isn't authenticated when running the `experimental.ai.prompt`
action, open the `HoverPane` with a message telling them how to (or a control
enabling them to)
* [ ] If the user **is** authenticated when running the `experimental.ai.prompt`
action, **BUT** bot authorized to use that model/billing/whatever, open the
`HoverPane` with a message explaining that / telling them how to.
* Thought process: Copilot is another fee on top of your GH subscription. You
might be able to log in with your GH account, but not be allowed to use
copilot.
* [ ]
## After Build Todo's
### 🚶 Walk
* [ ] Extensions can add custom `ShortcutAction`s to the Terminal
* [ ] Change the string for this action to something more final than `experimental.ai.prompt`
* [ ] Extensions can add UI elements to the Terminal window
* [ ] Extensions can request the Terminal open a `HoverPane` and specify the
content for that pane.
* [ ] Extensions can add `Page`s to the Terminal settins UI for their own settings
* [ ] The `AiPalette` control is moved out of the Terminal proper and into a
separate app package
* [ ] ...
### 🏃‍♂️ Run
> The AI Palette can be moved, resized while hovering
> The AI Palette can be docked from a hovering control to a Pane
* [ ] Enable the `HoverPane` control to be resizable with the mouse
* [ ] Enable the `HoverPane` control to be dragable with the mouse
* i.e., instead of being strictly docked to the left of the screen, it's got a
little grabby icon / titlebar that can be used to reposition it.
* [ ] Enable `Pane`s to host non-terminal content
* [ ] Add a button to`HoverPane` to cause it to be docked to the currently active pane
* this will open a new `auto` direction split, taking up whatever percent of
the parent is necessary to achieve the same size as the `HoverPane` had
before(?)
* [ ] ...
### 🚀 Sprint
* [ ] ...
## Conclusion
### Rejected ideas
**Option 1**: Use the [Suggestions UI] for this.
* **Pros**:
* the UI appears right at the place the user is typing, keeing them exactly in
the context they started in.
* Suggestion `source`s would be easy/cheap to add as an extension, with
relatively few Terminal changes (especially compared with adding
extension-specific actions)
* **Cons**:
* The model of prompting, then navigating results that are delivered
asynchronously, is fundamentally not compatible with the way the suggestions
UI works.
**Option 2**: Create a new Command Palette Mode for this. This was explored in greater detail
over in the [Extensions] doc.
* **Pros**: "cheap", we can just reuse the Command Palette for this. _Perfect_, right?
* **Cons**:
* Probably more expensive than it's worth to combine the functionality with
the Command Palette. Best to just start fresh with a new control that
doesn't need to carry the baggage of the other Command Palette modes.
* When this does end up being delivered as a separate package (extension), the
fullness of what we want to customize about this UX would be best served by
another UI element anyways. It'll be VERY expensive to instead expose knobs
for extensions to fully customize the existing palette.
### Future Considerations
The flexibility of the `HoverPane` to display arbitrary content could be
exceptionally useful in the future. All sorts of UI elements that we've had no
place to put before could be placed into `HoverPane`s. [#644], [#1595], and
[#8647] are all extension scenarios that would be able to leverage this.
## Resources
### Footnotes
<a name="footnote-1"><a>[1]:
[Fig]: https://fig.io/user-manual/ai
[Warp]: https://docs.warp.dev/features/entry/ai-command-search
[Copilot CLI]: https://githubnext.com/projects/copilot-cli/
[Terminal North Star]: ../Terminal-North-Star.md
[Tasks]: ../Tasks.md
[Shell Integration]: ../Shell-Integration-Marks.md
[Suggestions UI]: ../Suggestions-UI.md
[Extensions]: ../Suggestions-UI.md
[Overview]: ./Overview.md
[Implicit Suggestions]: ./Implicit-Suggestions.md
[Prompting]: ./Prompting.md
[Explain that]: ./Explain-that.md
<!-- TODO! -->
[shell-driven autocompletion]: ../Terminal-North-Star.md#Shell_autocompletion
[#977]: https://github.com/microsoft/terminal/issues/997
[#12861]: https://github.com/microsoft/terminal/issues/12861
[#4000]: https://github.com/microsoft/terminal/issues/4000
[#644]: https://github.com/microsoft/terminal/issues/644
[#1595]: https://github.com/microsoft/terminal/issues/1595
[#8647]: https://github.com/microsoft/terminal/issues/8647

View File

@@ -1,410 +0,0 @@
---
author: Mike Griese @zadjii-msft
created on: 2023-02-13
last updated: 2023-02-23
issue id: n/a
---
# Terminal AI Extensions
## Abstract
This is a quick and dirty description of how the Terminal could implement our AI
experiences using a extensible backend. This will allow the Terminal to iterat
on AI-powered experiences, without any dedicated AI code in the Terminal itself.
This enables multiple different AI models to be plugged in to the Terminal, each
hosted in their own app package. The Terminal will communicate with these
packages over a well-defined [App Service Connection].
- [Terminal AI Extensions](#terminal-ai-extensions)
- [Abstract](#abstract)
- [Solution Details](#solution-details)
- [Declaring the Extension \& Host](#declaring-the-extension--host)
- [Picking a backend](#picking-a-backend)
- [Establishing the connection](#establishing-the-connection)
- [Connection "API"](#connection-api)
- [Note on responses](#note-on-responses)
- [Prompting](#prompting)
- [Explain this](#explain-this)
- [User Experience and Design](#user-experience-and-design)
- [Potential Issues](#potential-issues)
- [Tenents](#tenents)
- [Before spec is done TODO!s](#before-spec-is-done-todos)
- [Future considerations](#future-considerations)
- [Resources](#resources)
- [Footnotes](#footnotes)
## Solution Details
Below is a very technical description of how we will put this support together.
For the remainder of this doc, we'll be using a hypothetical "GitHub Copilot for
the Terminal" extension for our examples. We'll cover first how the apps will
need to be manifested so they can communicate with one another. Then we'll
briefly touch on how Terminal can use this model to pick from different
extensions to choose it's AI model. Lastly, we'll decribe the API the Terminal
will use to communicate with these extensions.
![](./img/ai-providers-plus-powershell.png)
### Declaring the Extension & Host
Terminal becomes an app _service client_. It is also an app _extension host_. It
is gonna register as the host for `com.microsoft.terminal.aiHost` extensions in
the following way:
```xml
<uap3:Extension Category="windows.appExtensionHost">
<uap3:AppExtensionHost>
<uap3:Name>com.microsoft.terminal.aiHost</uap3:Name>
</uap3:AppExtensionHost>
</uap3:Extension>
```
The Github extension app registers as a `com.microsoft.terminal.aiHost`
extension. It also declares a `windows.appService`, which it will use to service
the extension. In the blob for `aiHost` extension, the App should add a property
indicating the name of the AppService that should be used for the extension. For
example:
```xml
<!-- <Package.Applications.Application.Extensions>... -->
<uap:Extension Category="windows.appService" EntryPoint="CopilotService.AiProviderTask">
<uap3:AppService Name="com.github.copilot.terminalAiProvider" />
</uap:Extension>
<uap3:Extension Category="windows.appExtension">
<uap3:AppExtension Name="com.microsoft.terminal.aiHost"
Id="GitHubCopilot"
DisplayName="GitHub Copilot"
Description="whatever"
PublicFolder="Public">
<uap3:Properties>
<ProviderName>com.github.copilot.terminalAiProvider</ProviderName>
</uap3:Properties>
</uap3:AppExtension>
</uap3:Extension>
```
Extension authors should then refer to [this
example](https://github.com/microsoft/Windows-universal-samples/blob/main/Samples/AppServices/cppwinrt/RandomNumberService/RandomNumberGeneratorTask.cpp)
for how they might implement the `Task` to handle these incoming requests.
### Picking a backend
Terminal will be able to enumerate the apps that implement the `aiHost`
extension. We'll use that as a list for a combobox in the settings to give users
a choice of which backend to choose (or to disable the experience entirely).
When we enumerate those packages, we'll get the `ProviderName` property out of
their manifest, and stash that, so we know how to build the app service
connection to that app. The code conhost & Terminal use today for defterm
already does something similar to get a clsid out of the manifest.
If the user chooses to set the chosen provider to "None", then when they invoke
one of the AI experiences, we'll simply inform them that no AI provider is set
up, and provide a deep link to the Settings UI to point them at where to pick
one.
### Establishing the connection
_[Sample code](https://github.com/microsoft/Windows-universal-samples/blob/ad9a0c4def222aaf044e51f8ee0939911cb58471/Samples/AppServices/cppwinrt/AppServicesClient/KeepConnectionOpenScenario.cpp#L52-L57)_
When the Terminal needs to invoke the AI provider, it will do so in the following fashion:
```c++
//Set up a new app service connection
connection = AppServiceConnection();
connection.AppServiceName(L"com.github.copilot.terminalAiProvider");
connection.PackageFamilyName(L"Microsoft.GithubWhatever.YouGet.ThePoint_8wekyb3d8bbwe");
connection.ServiceClosed({ get_weak(), &KeepConnectionOpenScenario::Connection_ServiceClosed });
AppServiceConnectionStatus status = co_await connection.OpenAsync();
```
This will create an `AppServiceConnection` that the Terminal can use to pass
`ValueSet` messages to the extension provider. These messages aren't great for
any sort of real-time communication, but are performant enough for "the user
clicked a button, now they want a response back".
Once we've got a connection established, we'll need to establish that the app is
authenticated, before beginning to send queries to that connection. TODO! how?
### Connection "API"
> [!IMPORTANT]
>
> TODO!
>
> This section was authored at the start of 2023. We since moved from
> just "a list of commands" to a more chat-like experience. This section is
> super out of date.
>
Terminal will fire off a `ValueSet`s to the provider to perform various tasks we
need[^1]. Depending on what's needed, we'll send different requests, with
different expected payload.
Terminal will only communicate messages on predefined "verbs". This will allow
the Terminal to build its UI and experience regardless of how the backend has
decided to implement its own API. So long as the backend AI provider implements
this API interface, the Terminal will be able to build a consistent UI
experience.
Terminal will keep its manipulation of the input request to a minimum. It is up
to each model provider to craft how it wants to handle each scenario. Different
models might have different APIs for requests and responses. Different apps may
want to customize the context that they provide with the prompt the user typed,
to give more relevant responses. The Terminal tries to not declare how each
extension should interface with a particular AI backend. Instead, the Terminal
only provides a general description of what it would like to happen.
#### Note on responses
In each response below, there's a `result` and `message` property returned to
the Terminal. This allows the app to indicate some sort of error message to the
user. This will likely be most used for authentication errors and network
errors.
In those cases, the Terminal will be able to provide dedicated UI messages to
indicate the error. For example, in the case of an authentication failure, the
Terminal may provide a button to send a message to the service host so that it
can re-authenticate the user.
Or, perhaps, the user might be authenticated, but might not have a particular AI
experience enabled for their account. The Terminal could similarly provide a
button to prompt the user to remedy this. TODO! should we? Or is that the
responsibility of the extension?
#### Prompting
<table>
<thead>
<td>Request</td>
<td>Response</td>
</thead>
<tr>
<td>
```ts
{
"verb": "promptForCommands",
"prompt": string,
"context": {}
}
```
</td>
<td>
```ts
{
"verb": "promptForCommands",
"result": number,
"message": string
"commands": string[],
}
```
</td>
</tr>
</table>
**Purpose**: The `prompt` is a natural-language description of a command to run.
The provider should take this and turn it into a list of `commands` that the
user could run. The `commands` should be commandlines that could be directly ran
at the prompt, without any additional context accompanying them.
We could theoretically put command history in `context`, if that's not PII / if
we're allowed to. That might help refine results. For example, knowing if the
commandline should be a CMD/PowerShell/bash (or other \*nix-like shell) would
greatly refine results.
#### Explain this
<table>
<thead>
<td>Request</td>
<td>Response</td>
</thead>
<tr>
<td>
```ts
{
"verb": "explainThis",
"prompt": string,
"context": {}
}
```
</td>
<td>
```ts
{
"verb": "promptForCommands",
"result": number,
"message": string
"response": string,
}
```
</td>
</tr>
</table>
**Purpose**: The `prompt` is a string of text in the user's terminal. They would
like more information on what it means.
We could theoretically put additional command history in `context`, if that's
not PII / if we're allowed to. Most specifically, I think it might be helpful to
give the entirety of the command that the prompt appeared in, if that's known to
us. Again, that might be PII.
This could be used in two contexts:
* A terminal-buffer initiated "what does this mean" scenario. This could be
something like:
* The user selected some text in the buffer and wants to know what it means
* A command exited with an error, and the Terminal provided a shortcut to
inquire what that particular error means.
* A UI-driven "I need some help" scenario. This is a more ChatGPT-like
experience. The user wants to know more about something, with more context
than just commands as responses.
## User Experience and Design
![](./img/llm-providers-settings-000.png)
_programmer art mockup of settings page displaying list of available Terminal LLM providers_
Presumably then each drill-in page would have individual settings Terminal can
then control for each provider. For example, controlling permissions to what the
plugin can or cannot do / access
## Potential Issues
* [ ] TODO! Branding - how do we want to allow individual providers to specify
branding elements in the AI experiences? I'm thinking things like title text,
logos, etc.
* [ ] TODO! As noted above - how exactly should authentication/subscription
failures be handled?
* Do we need a dedicated "authenticate" verb on the API?
* [ ] TODO! Determine how much additional context can be sent to extensions.
* [ ] TODO! We need to also add a way for Terminal to securely store the allowed
permissions per-provider. For example, if we're even thinking about providing
profile/commandline/history/command context to the provider, the user needs to
be able to disable that on a per-provider basis.
* [ ] TODO! ...
### Tenents
<table>
<tr><td><strong>Sustainability</strong></td><td>
[comment]: # What kind of impacts, if any, will this feature have on the environment?
It's not good, that's for sure.
* This [source] estimated a single ChatGPT query at 6.79 Wh.
* An IPhone 15 has a 12.98 Wh battery
* So a single query is like, .5 phone batteries of power.
* According to [the EIA], the US contributes 0.86 pounds of CO2 per kWh
* Napkin math: We've got 1M users with one query a day. (Obviously, it might be
more users with fewer queries, or fewer with more.)
* That's (6.79Wh * 1000000/day) = 6790000 Wh = 6790 kWh / day
* That's (6790kWh * 0.86 lb CO2 / kWh) = 5839.4 lbs CO2 / day
* = 2.64870729 metric tons CO2 / day
* = 966.77816085 tons/year
Author note: I'd rather not build a product that adds measurable tons of CO2 a
day. Not sure how we can justify this until the power consumption of LLMs comes
down dramatically.
<tr><td><strong>Privacy</strong></td><td>
[comment]: # How will user data be handled? What data will be shared with extension providers?
Terminal will present to users a number of settings to control how much context plugins are able to recieve from the Terminal.
* Currently selected text
* Currently typed commandline
* Most recent (N) command(s)
* and errorlevels
* and output
* Environment variables
* profile commandline(?) (we may want to always provide the target exe, without args, as a bare min)
* Other panes too?
TODO! This list is incomplete; you can help by adding missing items
</td></tr>
<tr><td><strong>Accessibility</strong></td><td>
[comment]: # How will the proposed change impact accessibility for users of screen readers, assistive input devices, etc.
</td></tr>
<tr><td><strong>Security</strong></td><td>
[comment]: # How will the proposed change impact security?
Terminal will have per-provider settings that it controls OUTSIDE of
`settings.json` that controls the permissions for each individual plugin. This
will ensure that plugins do not grant themselves additional permissions by
writing to the Terminal's settings themselves.
</td></tr>
<tr><td><strong>Reliability</strong></td><td>
[comment]: # Will the proposed change improve reliability? If not, why make the change?
</td></tr>
<tr><td><strong>Compatibility</strong></td><td>
[comment]: # Will the proposed change break existing code/behaviors? If so, how, and is the breaking change "worth it"?
</td></tr>
<tr><td><strong>Performance, Power, and Efficiency</strong></td><td>
[comment]: # Will the proposed change
</td></tr>
</table>
## Before spec is done TODO!s
* [ ] TODO! PowerShell folks would like to have the connection be two-way. Can we have extensions invoke experiences in the Terminal?
* [ ] TODO! add interface for Terminal to query what providers are available in each terminal extension
- I think we should do that within a single `uap3:AppExtension`, so that apps
can change the list of providers on the fly, without an update to the app
package
* [ ] ...
## Future considerations
* Maybe it'd be cool if profiles could specify a default LLM provider? So if you
opened the chat / whatever with that pane active, we'd default to that
provider, rather than the one that is otherwise selected as the default?
## Resources
* The [App Service Connection Sample](https://github.com/Microsoft/Windows-universal-samples/tree/main/Samples/AppServices) is basically mandatory reading for how this will work.
### Footnotes
[^1]: A ValueSet isn't exactly JSON, but it is close enough that I'm gonna use it for simplicity
[App Service Connection]: https://learn.microsoft.com/en-us/windows/uwp/launch-resume/how-to-create-and-consume-an-app-service
[source]: https://medium.com/@zodhyatech/how-much-energy-does-chatgpt-consume-4cba1a7aef85
[the EIA]: https://www.eia.gov/tools/faqs/faq.php?id=74&t=11

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -404,18 +404,6 @@ til::CoordType ROW::AdjustToGlyphStart(til::CoordType column) const noexcept
return _adjustBackward(_clampedColumn(column));
}
// Returns the (exclusive) ending column of the glyph at the given column.
// In other words, if you have 3 wide glyphs
// AA BB CC
// 01 23 45 <-- column
// Examples:
// - `AdjustToGlyphEnd(4)` returns 6.
// - `AdjustToGlyphEnd(3)` returns 4.
til::CoordType ROW::AdjustToGlyphEnd(til::CoordType column) const noexcept
{
return _adjustForward(_clampedColumnInclusive(column));
}
// Routine Description:
// - clears char data in column in row
// Arguments:
@@ -951,10 +939,36 @@ uint16_t ROW::size() const noexcept
return _columnCount;
}
// Routine Description:
// - Retrieves the column that is one after the last non-space character in the row.
til::CoordType ROW::GetLastNonSpaceColumn() const noexcept
til::CoordType ROW::MeasureLeft() const noexcept
{
const auto text = GetText();
const auto beg = text.begin();
const auto end = text.end();
auto it = beg;
for (; it != end; ++it)
{
if (*it != L' ')
{
break;
}
}
return gsl::narrow_cast<til::CoordType>(it - beg);
}
til::CoordType ROW::MeasureRight() const noexcept
{
if (_wrapForced)
{
auto width = _columnCount;
if (_doubleBytePadded)
{
width--;
}
return width;
}
const auto text = GetText();
const auto beg = text.begin();
const auto end = text.end();
@@ -974,42 +988,7 @@ til::CoordType ROW::GetLastNonSpaceColumn() const noexcept
//
// An example: The row is 10 cells wide and `it` points to the second character.
// `it - beg` would return 1, but it's possible it's actually 1 wide glyph and 8 whitespace.
return gsl::narrow_cast<til::CoordType>(GetReadableColumnCount() - (end - it));
}
til::CoordType ROW::MeasureLeft() const noexcept
{
const auto text = GetText();
const auto beg = text.begin();
const auto end = text.end();
auto it = beg;
for (; it != end; ++it)
{
if (*it != L' ')
{
break;
}
}
return gsl::narrow_cast<til::CoordType>(it - beg);
}
// Routine Description:
// - Retrieves the column that is one after the last valid character in the row.
til::CoordType ROW::MeasureRight() const noexcept
{
if (_wrapForced)
{
auto width = _columnCount;
if (_doubleBytePadded)
{
width--;
}
return width;
}
return GetLastNonSpaceColumn();
return gsl::narrow_cast<til::CoordType>(_columnCount - (end - it));
}
bool ROW::ContainsText() const noexcept

View File

@@ -137,7 +137,6 @@ public:
til::CoordType NavigateToPrevious(til::CoordType column) const noexcept;
til::CoordType NavigateToNext(til::CoordType column) const noexcept;
til::CoordType AdjustToGlyphStart(til::CoordType column) const noexcept;
til::CoordType AdjustToGlyphEnd(til::CoordType column) const noexcept;
void ClearCell(til::CoordType column);
OutputCellIterator WriteCells(OutputCellIterator it, til::CoordType columnBegin, std::optional<bool> wrap = std::nullopt, std::optional<til::CoordType> limitRight = std::nullopt);
@@ -152,7 +151,6 @@ public:
TextAttribute GetAttrByColumn(til::CoordType column) const;
std::vector<uint16_t> GetHyperlinks() const;
uint16_t size() const noexcept;
til::CoordType GetLastNonSpaceColumn() const noexcept;
til::CoordType MeasureLeft() const noexcept;
til::CoordType MeasureRight() const noexcept;
bool ContainsText() const noexcept;

View File

@@ -126,8 +126,6 @@ void TextBuffer::_reserve(til::size screenBufferSize, const TextAttribute& defau
// The compiler doesn't understand the likelihood of our branches. (PGO does, but that's imperfect.)
__declspec(noinline) void TextBuffer::_commit(const std::byte* row)
{
assert(row >= _commitWatermark);
const auto rowEnd = row + _bufferRowStride;
const auto remaining = gsl::narrow_cast<uintptr_t>(_bufferEnd - _commitWatermark);
const auto minimum = gsl::narrow_cast<uintptr_t>(rowEnd - _commitWatermark);
@@ -148,7 +146,7 @@ void TextBuffer::_decommit() noexcept
_commitWatermark = _buffer.get();
}
// Constructs ROWs between [_commitWatermark,until).
// Constructs ROWs up to (excluding) the ROW pointed to by `until`.
void TextBuffer::_construct(const std::byte* until) noexcept
{
for (; _commitWatermark < until; _commitWatermark += _bufferRowStride)
@@ -160,7 +158,8 @@ void TextBuffer::_construct(const std::byte* until) noexcept
}
}
// Destructs ROWs between [_buffer,_commitWatermark).
// Destroys all previously constructed ROWs.
// Be careful! This doesn't reset any of the members, in particular the _commitWatermark.
void TextBuffer::_destroy() const noexcept
{
for (auto it = _buffer.get(); it < _commitWatermark; it += _bufferRowStride)
@@ -169,8 +168,9 @@ void TextBuffer::_destroy() const noexcept
}
}
// This function is "direct" because it trusts the caller to properly
// wrap the "offset" parameter modulo the _height of the buffer.
// This function is "direct" because it trusts the caller to properly wrap the "offset"
// parameter modulo the _height of the buffer, etc. But keep in mind that a offset=0
// is the GetScratchpadRow() and not the GetRowByOffset(0). That one is offset=1.
ROW& TextBuffer::_getRowByOffsetDirect(size_t offset)
{
const auto row = _buffer.get() + _bufferRowStride * offset;
@@ -184,7 +184,6 @@ ROW& TextBuffer::_getRowByOffsetDirect(size_t offset)
return *reinterpret_cast<ROW*>(row);
}
// See GetRowByOffset().
ROW& TextBuffer::_getRow(til::CoordType y) const
{
// Rows are stored circularly, so the index you ask for is offset by the start position and mod the total of rows.
@@ -198,7 +197,6 @@ ROW& TextBuffer::_getRow(til::CoordType y) const
}
// We add 1 to the row offset, because row "0" is the one returned by GetScratchpadRow().
// See GetScratchpadRow() for more explanation.
#pragma warning(suppress : 26492) // Don't use const_cast to cast away const or volatile (type.3).
return const_cast<TextBuffer*>(this)->_getRowByOffsetDirect(gsl::narrow_cast<size_t>(offset) + 1);
}
@@ -240,9 +238,6 @@ ROW& TextBuffer::GetScratchpadRow()
// Returns a row filled with whitespace and the given attributes, for you to freely use.
ROW& TextBuffer::GetScratchpadRow(const TextAttribute& attributes)
{
// The scratchpad row is mapped to the underlying index 0, whereas all regular rows are mapped to
// index 1 and up. We do it this way instead of the other way around (scratchpad row at index _height),
// because that would force us to MEM_COMMIT the entire buffer whenever this function is called.
auto& r = _getRowByOffsetDirect(0);
r.Reset(attributes);
return r;
@@ -907,14 +902,15 @@ til::point TextBuffer::GetLastNonSpaceCharacter(const Viewport* viewOptional) co
// If the X coordinate turns out to be -1, the row was empty, we need to search backwards for the real end of text.
const auto viewportTop = viewport.Top();
// while (this row is empty, and we're not at the top)
while (coordEndOfText.x < 0 && coordEndOfText.y > viewportTop)
auto fDoBackUp = (coordEndOfText.x < 0 && coordEndOfText.y > viewportTop); // this row is empty, and we're not at the top
while (fDoBackUp)
{
coordEndOfText.y--;
const auto& backupRow = GetRowByOffset(coordEndOfText.y);
// We need to back up to the previous row if this line is empty, AND there are more rows
coordEndOfText.x = backupRow.MeasureRight() - 1;
fDoBackUp = (coordEndOfText.x < 0 && coordEndOfText.y > viewportTop);
}
// don't allow negative results
@@ -1150,39 +1146,6 @@ void TextBuffer::Reset() noexcept
_initialAttributes = _currentAttributes;
}
void TextBuffer::ClearScrollback(const til::CoordType start, const til::CoordType height)
{
if (start <= 0)
{
return;
}
if (height <= 0)
{
_decommit();
return;
}
// Our goal is to move the viewport to the absolute start of the underlying memory buffer so that we can
// MEM_DECOMMIT the remaining memory. _firstRow is used to make the TextBuffer behave like a circular buffer.
// The start parameter is relative to the _firstRow. The trick to get the content to the absolute start
// is to simply add _firstRow ourselves and then reset it to 0. This causes ScrollRows() to write into
// the absolute start while reading from relative coordinates. This works because GetRowByOffset()
// operates modulo the buffer height and so the possibly-too-large startAbsolute won't be an issue.
const auto startAbsolute = _firstRow + start;
_firstRow = 0;
ScrollRows(startAbsolute, height, -startAbsolute);
const auto end = _estimateOffsetOfLastCommittedRow();
for (auto y = height; y <= end; ++y)
{
GetMutableRowByOffset(y).Reset(_initialAttributes);
}
ScrollMarks(-start);
ClearMarksInRange(til::point{ 0, height }, til::point{ _width, _height });
}
// Routine Description:
// - This is the legacy screen resize with minimal changes
// Arguments:
@@ -1953,6 +1916,135 @@ void TextBuffer::_ExpandTextRow(til::inclusive_rect& textRow) const
}
}
// Routine Description:
// - Retrieves the text data from the selected region and presents it in a clipboard-ready format (given little post-processing).
// Arguments:
// - includeCRLF - inject CRLF pairs to the end of each line
// - trimTrailingWhitespace - remove the trailing whitespace at the end of each line
// - textRects - the rectangular regions from which the data will be extracted from the buffer (i.e.: selection rects)
// - GetAttributeColors - function used to map TextAttribute to RGB COLORREFs. If null, only extract the text.
// - formatWrappedRows - if set we will apply formatting (CRLF inclusion and whitespace trimming) on wrapped rows
// Return Value:
// - The text, background color, and foreground color data of the selected region of the text buffer.
const TextBuffer::TextAndColor TextBuffer::GetText(const bool includeCRLF,
const bool trimTrailingWhitespace,
const std::vector<til::inclusive_rect>& selectionRects,
std::function<std::pair<COLORREF, COLORREF>(const TextAttribute&)> GetAttributeColors,
const bool formatWrappedRows) const
{
TextAndColor data;
const auto copyTextColor = GetAttributeColors != nullptr;
// preallocate our vectors to reduce reallocs
const auto rows = selectionRects.size();
data.text.reserve(rows);
if (copyTextColor)
{
data.FgAttr.reserve(rows);
data.BkAttr.reserve(rows);
}
// for each row in the selection
for (size_t i = 0; i < rows; i++)
{
const auto iRow = selectionRects.at(i).top;
const auto highlight = Viewport::FromInclusive(selectionRects.at(i));
// retrieve the data from the screen buffer
auto it = GetCellDataAt(highlight.Origin(), highlight);
// allocate a string buffer
std::wstring selectionText;
std::vector<COLORREF> selectionFgAttr;
std::vector<COLORREF> selectionBkAttr;
// preallocate to avoid reallocs
selectionText.reserve(gsl::narrow<size_t>(highlight.Width()) + 2); // + 2 for \r\n if we munged it
if (copyTextColor)
{
selectionFgAttr.reserve(gsl::narrow<size_t>(highlight.Width()) + 2);
selectionBkAttr.reserve(gsl::narrow<size_t>(highlight.Width()) + 2);
}
// copy char data into the string buffer, skipping trailing bytes
while (it)
{
const auto& cell = *it;
if (cell.DbcsAttr() != DbcsAttribute::Trailing)
{
const auto chars = cell.Chars();
selectionText.append(chars);
if (copyTextColor)
{
const auto cellData = cell.TextAttr();
const auto [CellFgAttr, CellBkAttr] = GetAttributeColors(cellData);
for (size_t j = 0; j < chars.size(); ++j)
{
selectionFgAttr.push_back(CellFgAttr);
selectionBkAttr.push_back(CellBkAttr);
}
}
}
++it;
}
// We apply formatting to rows if the row was NOT wrapped or formatting of wrapped rows is allowed
const auto shouldFormatRow = formatWrappedRows || !GetRowByOffset(iRow).WasWrapForced();
if (trimTrailingWhitespace)
{
if (shouldFormatRow)
{
// remove the spaces at the end (aka trim the trailing whitespace)
while (!selectionText.empty() && selectionText.back() == UNICODE_SPACE)
{
selectionText.pop_back();
if (copyTextColor)
{
selectionFgAttr.pop_back();
selectionBkAttr.pop_back();
}
}
}
}
// apply CR/LF to the end of the final string, unless we're the last line.
// a.k.a if we're earlier than the bottom, then apply CR/LF.
if (includeCRLF && i < selectionRects.size() - 1)
{
if (shouldFormatRow)
{
// then we can assume a CR/LF is proper
selectionText.push_back(UNICODE_CARRIAGERETURN);
selectionText.push_back(UNICODE_LINEFEED);
if (copyTextColor)
{
// can't see CR/LF so just use black FG & BK
const auto Blackness = RGB(0x00, 0x00, 0x00);
selectionFgAttr.push_back(Blackness);
selectionFgAttr.push_back(Blackness);
selectionBkAttr.push_back(Blackness);
selectionBkAttr.push_back(Blackness);
}
}
}
data.text.emplace_back(std::move(selectionText));
if (copyTextColor)
{
data.FgAttr.emplace_back(std::move(selectionFgAttr));
data.BkAttr.emplace_back(std::move(selectionBkAttr));
}
}
return data;
}
size_t TextBuffer::SpanLength(const til::point coordStart, const til::point coordEnd) const
{
const auto bufferSize = GetSize();
@@ -1991,292 +2083,186 @@ std::wstring TextBuffer::GetPlainText(const til::point& start, const til::point&
}
// Routine Description:
// - Given a copy request and a row, retrieves the row bounds [begin, end) and
// a boolean indicating whether a line break should be added to this row.
// - Generates a CF_HTML compliant structure based on the passed in text and color data
// Arguments:
// - req - the copy request
// - iRow - the row index
// - row - the row
// Return Value:
// - The row bounds and a boolean for line break
std::tuple<til::CoordType, til::CoordType, bool> TextBuffer::_RowCopyHelper(const TextBuffer::CopyRequest& req, const til::CoordType iRow, const ROW& row) const
{
til::CoordType rowBeg = 0;
til::CoordType rowEnd = 0;
if (req.blockSelection)
{
const auto lineRendition = row.GetLineRendition();
const auto minX = req.bufferCoordinates ? req.minX : ScreenToBufferLine(til::point{ req.minX, iRow }, lineRendition).x;
const auto maxX = req.bufferCoordinates ? req.maxX : ScreenToBufferLine(til::point{ req.maxX, iRow }, lineRendition).x;
rowBeg = minX;
rowEnd = maxX + 1; // +1 to get an exclusive end
}
else
{
const auto lineRendition = row.GetLineRendition();
const auto beg = req.bufferCoordinates ? req.beg : ScreenToBufferLine(req.beg, lineRendition);
const auto end = req.bufferCoordinates ? req.end : ScreenToBufferLine(req.end, lineRendition);
rowBeg = iRow != beg.y ? 0 : beg.x;
rowEnd = iRow != end.y ? row.GetReadableColumnCount() : end.x + 1; // +1 to get an exclusive end
}
// Our selection mechanism doesn't stick to glyph boundaries at the moment.
// We need to adjust begin and end points manually to avoid partially
// selected glyphs.
rowBeg = row.AdjustToGlyphStart(rowBeg);
rowEnd = row.AdjustToGlyphEnd(rowEnd);
// When `formatWrappedRows` is set, apply formatting on all rows (wrapped
// and non-wrapped), but when it's false, format non-wrapped rows only.
const auto shouldFormatRow = req.formatWrappedRows || !row.WasWrapForced();
// trim trailing whitespace
if (shouldFormatRow && req.trimTrailingWhitespace)
{
rowEnd = std::min(rowEnd, row.GetLastNonSpaceColumn());
}
// line breaks
const auto addLineBreak = shouldFormatRow && req.includeLineBreak;
return { rowBeg, rowEnd, addLineBreak };
}
// Routine Description:
// - Retrieves the text data from the buffer and presents it in a clipboard-ready format.
// Arguments:
// - req - the copy request having the bounds of the selected region and other related configuration flags.
// Return Value:
// - The text data from the selected region of the text buffer. Empty if the copy request is invalid.
std::wstring TextBuffer::GetPlainText(const CopyRequest& req) const
{
if (req.beg > req.end)
{
return {};
}
std::wstring selectedText;
for (auto iRow = req.beg.y; iRow <= req.end.y; ++iRow)
{
const auto& row = GetRowByOffset(iRow);
const auto& [rowBeg, rowEnd, addLineBreak] = _RowCopyHelper(req, iRow, row);
// save selected text
selectedText += row.GetText(rowBeg, rowEnd);
if (addLineBreak && iRow != req.end.y)
{
selectedText += L"\r\n";
}
}
return selectedText;
}
// Routine Description:
// - Generates a CF_HTML compliant structure from the selected region of the buffer
// Arguments:
// - req - the copy request having the bounds of the selected region and other related configuration flags.
// - rows - the text and color data we will format & encapsulate
// - backgroundColor - default background color for characters, also used in padding
// - fontHeightPoints - the unscaled font height
// - fontFaceName - the name of the font used
// - backgroundColor - default background color for characters, also used in padding
// - isIntenseBold - true if being intense is treated as being bold
// - GetAttributeColors - function to get the colors of the text attributes as they're rendered
// Return Value:
// - string containing the generated HTML. Empty if the copy request is invalid.
std::string TextBuffer::GenHTML(const CopyRequest& req,
// - string containing the generated HTML
std::string TextBuffer::GenHTML(const TextAndColor& rows,
const int fontHeightPoints,
const std::wstring_view fontFaceName,
const COLORREF backgroundColor,
const bool isIntenseBold,
std::function<std::tuple<COLORREF, COLORREF, COLORREF>(const TextAttribute&)> GetAttributeColors) const noexcept
const COLORREF backgroundColor)
{
// GH#5347 - Don't provide a title for the generated HTML, as many
// web applications will paste the title first, followed by the HTML
// content, which is unexpected.
if (req.beg > req.end)
{
return {};
}
try
{
std::string htmlBuilder;
std::ostringstream htmlBuilder;
// First we have to add some standard HTML boiler plate required for
// CF_HTML as part of the HTML Clipboard format
constexpr std::string_view htmlHeader = "<!DOCTYPE><HTML><HEAD></HEAD><BODY>";
htmlBuilder += htmlHeader;
// First we have to add some standard
// HTML boiler plate required for CF_HTML
// as part of the HTML Clipboard format
const std::string htmlHeader =
"<!DOCTYPE><HTML><HEAD></HEAD><BODY>";
htmlBuilder << htmlHeader;
htmlBuilder += "<!--StartFragment -->";
htmlBuilder << "<!--StartFragment -->";
// apply global style in div element
{
htmlBuilder += "<DIV STYLE=\"";
htmlBuilder += "display:inline-block;";
htmlBuilder += "white-space:pre;";
fmt::format_to(std::back_inserter(htmlBuilder), FMT_COMPILE("background-color:{};"), Utils::ColorToHexString(backgroundColor));
htmlBuilder << "<DIV STYLE=\"";
htmlBuilder << "display:inline-block;";
htmlBuilder << "white-space:pre;";
htmlBuilder << "background-color:";
htmlBuilder << Utils::ColorToHexString(backgroundColor);
htmlBuilder << ";";
htmlBuilder << "font-family:";
htmlBuilder << "'";
htmlBuilder << ConvertToA(CP_UTF8, fontFaceName);
htmlBuilder << "',";
// even with different font, add monospace as fallback
fmt::format_to(std::back_inserter(htmlBuilder), FMT_COMPILE("font-family:'{}',monospace;"), til::u16u8(fontFaceName));
htmlBuilder << "monospace;";
fmt::format_to(std::back_inserter(htmlBuilder), FMT_COMPILE("font-size:{}pt;"), fontHeightPoints);
htmlBuilder << "font-size:";
htmlBuilder << fontHeightPoints;
htmlBuilder << "pt;";
// note: MS Word doesn't support padding (in this way at least)
// todo: customizable padding
htmlBuilder += "padding:4px;";
htmlBuilder << "padding:";
htmlBuilder << 4; // todo: customizable padding
htmlBuilder << "px;";
htmlBuilder += "\">";
htmlBuilder << "\">";
}
for (auto iRow = req.beg.y; iRow <= req.end.y; ++iRow)
// copy text and info color from buffer
auto hasWrittenAnyText = false;
std::optional<COLORREF> fgColor = std::nullopt;
std::optional<COLORREF> bkColor = std::nullopt;
for (size_t row = 0; row < rows.text.size(); row++)
{
const auto& row = GetRowByOffset(iRow);
const auto [rowBeg, rowEnd, addLineBreak] = _RowCopyHelper(req, iRow, row);
const auto rowBegU16 = gsl::narrow_cast<uint16_t>(rowBeg);
const auto rowEndU16 = gsl::narrow_cast<uint16_t>(rowEnd);
const auto runs = row.Attributes().slice(rowBegU16, rowEndU16).runs();
size_t startOffset = 0;
auto x = rowBegU16;
for (const auto& [attr, length] : runs)
if (row != 0)
{
const auto nextX = gsl::narrow_cast<uint16_t>(x + length);
const auto [fg, bg, ul] = GetAttributeColors(attr);
const auto fgHex = Utils::ColorToHexString(fg);
const auto bgHex = Utils::ColorToHexString(bg);
const auto ulHex = Utils::ColorToHexString(ul);
const auto ulStyle = attr.GetUnderlineStyle();
const auto isUnderlined = ulStyle != UnderlineStyle::NoUnderline;
const auto isCrossedOut = attr.IsCrossedOut();
const auto isOverlined = attr.IsOverlined();
htmlBuilder += "<SPAN STYLE=\"";
fmt::format_to(std::back_inserter(htmlBuilder), FMT_COMPILE("color:{};"), fgHex);
fmt::format_to(std::back_inserter(htmlBuilder), FMT_COMPILE("background-color:{};"), bgHex);
if (isIntenseBold && attr.IsIntense())
{
htmlBuilder += "font-weight:bold;";
}
if (attr.IsItalic())
{
htmlBuilder += "font-style:italic;";
}
if (isCrossedOut || isOverlined)
{
fmt::format_to(std::back_inserter(htmlBuilder),
FMT_COMPILE("text-decoration:{} {} {};"),
isCrossedOut ? "line-through" : "",
isOverlined ? "overline" : "",
fgHex);
}
if (isUnderlined)
{
// Since underline, overline and strikethrough use the same css property,
// we cannot apply different colors to them at the same time. However, we
// can achieve the desired result by creating a nested <span> and applying
// underline style and color to it.
htmlBuilder += "\"><SPAN STYLE=\"";
switch (ulStyle)
{
case UnderlineStyle::NoUnderline:
break;
case UnderlineStyle::DoublyUnderlined:
fmt::format_to(std::back_inserter(htmlBuilder), FMT_COMPILE("text-decoration:underline double {};"), ulHex);
break;
case UnderlineStyle::CurlyUnderlined:
fmt::format_to(std::back_inserter(htmlBuilder), FMT_COMPILE("text-decoration:underline wavy {};"), ulHex);
break;
case UnderlineStyle::DottedUnderlined:
fmt::format_to(std::back_inserter(htmlBuilder), FMT_COMPILE("text-decoration:underline dotted {};"), ulHex);
break;
case UnderlineStyle::DashedUnderlined:
fmt::format_to(std::back_inserter(htmlBuilder), FMT_COMPILE("text-decoration:underline dashed {};"), ulHex);
break;
case UnderlineStyle::SinglyUnderlined:
default:
fmt::format_to(std::back_inserter(htmlBuilder), FMT_COMPILE("text-decoration:underline {};"), ulHex);
break;
}
}
htmlBuilder += "\">";
// text
std::string unescapedText;
THROW_IF_FAILED(til::u16u8(row.GetText(x, nextX), unescapedText));
for (const auto c : unescapedText)
{
switch (c)
{
case '<':
htmlBuilder += "&lt;";
break;
case '>':
htmlBuilder += "&gt;";
break;
case '&':
htmlBuilder += "&amp;";
break;
default:
htmlBuilder += c;
}
}
if (isUnderlined)
{
// close the nested span we created for underline
htmlBuilder += "</SPAN>";
}
htmlBuilder += "</SPAN>";
// advance to next run of text
x = nextX;
htmlBuilder << "<BR>";
}
// never add line break to the last row.
if (addLineBreak && iRow < req.end.y)
for (size_t col = 0; col < rows.text.at(row).length(); col++)
{
htmlBuilder += "<BR>";
const auto writeAccumulatedChars = [&](bool includeCurrent) {
if (col >= startOffset)
{
const auto unescapedText = ConvertToA(CP_UTF8, std::wstring_view(rows.text.at(row)).substr(startOffset, col - startOffset + includeCurrent));
for (const auto c : unescapedText)
{
switch (c)
{
case '<':
htmlBuilder << "&lt;";
break;
case '>':
htmlBuilder << "&gt;";
break;
case '&':
htmlBuilder << "&amp;";
break;
default:
htmlBuilder << c;
}
}
startOffset = col;
}
};
if (rows.text.at(row).at(col) == '\r' || rows.text.at(row).at(col) == '\n')
{
// do not include \r nor \n as they don't have color attributes
// and are not HTML friendly. For line break use '<BR>' instead.
writeAccumulatedChars(false);
break;
}
auto colorChanged = false;
if (!fgColor.has_value() || rows.FgAttr.at(row).at(col) != fgColor.value())
{
fgColor = rows.FgAttr.at(row).at(col);
colorChanged = true;
}
if (!bkColor.has_value() || rows.BkAttr.at(row).at(col) != bkColor.value())
{
bkColor = rows.BkAttr.at(row).at(col);
colorChanged = true;
}
if (colorChanged)
{
writeAccumulatedChars(false);
if (hasWrittenAnyText)
{
htmlBuilder << "</SPAN>";
}
htmlBuilder << "<SPAN STYLE=\"";
htmlBuilder << "color:";
htmlBuilder << Utils::ColorToHexString(fgColor.value());
htmlBuilder << ";";
htmlBuilder << "background-color:";
htmlBuilder << Utils::ColorToHexString(bkColor.value());
htmlBuilder << ";";
htmlBuilder << "\">";
}
hasWrittenAnyText = true;
// if this is the last character in the row, flush the whole row
if (col == rows.text.at(row).length() - 1)
{
writeAccumulatedChars(true);
}
}
}
htmlBuilder += "</DIV>";
if (hasWrittenAnyText)
{
// last opened span wasn't closed in loop above, so close it now
htmlBuilder << "</SPAN>";
}
htmlBuilder += "<!--EndFragment -->";
htmlBuilder << "</DIV>";
htmlBuilder << "<!--EndFragment -->";
constexpr std::string_view HtmlFooter = "</BODY></HTML>";
htmlBuilder += HtmlFooter;
htmlBuilder << HtmlFooter;
// once filled with values, there will be exactly 157 bytes in the clipboard header
constexpr size_t ClipboardHeaderSize = 157;
// these values are byte offsets from start of clipboard
const auto htmlStartPos = ClipboardHeaderSize;
const auto htmlEndPos = ClipboardHeaderSize + gsl::narrow<size_t>(htmlBuilder.length());
const auto htmlEndPos = ClipboardHeaderSize + gsl::narrow<size_t>(htmlBuilder.tellp());
const auto fragStartPos = ClipboardHeaderSize + gsl::narrow<size_t>(htmlHeader.length());
const auto fragEndPos = htmlEndPos - HtmlFooter.length();
// header required by HTML 0.9 format
std::string clipHeaderBuilder;
clipHeaderBuilder += "Version:0.9\r\n";
fmt::format_to(std::back_inserter(clipHeaderBuilder), FMT_COMPILE("StartHTML:{:0>10}\r\n"), htmlStartPos);
fmt::format_to(std::back_inserter(clipHeaderBuilder), FMT_COMPILE("EndHTML:{:0>10}\r\n"), htmlEndPos);
fmt::format_to(std::back_inserter(clipHeaderBuilder), FMT_COMPILE("StartFragment:{:0>10}\r\n"), fragStartPos);
fmt::format_to(std::back_inserter(clipHeaderBuilder), FMT_COMPILE("EndFragment:{:0>10}\r\n"), fragEndPos);
fmt::format_to(std::back_inserter(clipHeaderBuilder), FMT_COMPILE("StartSelection:{:0>10}\r\n"), fragStartPos);
fmt::format_to(std::back_inserter(clipHeaderBuilder), FMT_COMPILE("EndSelection:{:0>10}\r\n"), fragEndPos);
std::ostringstream clipHeaderBuilder;
clipHeaderBuilder << "Version:0.9\r\n";
clipHeaderBuilder << std::setfill('0');
clipHeaderBuilder << "StartHTML:" << std::setw(10) << htmlStartPos << "\r\n";
clipHeaderBuilder << "EndHTML:" << std::setw(10) << htmlEndPos << "\r\n";
clipHeaderBuilder << "StartFragment:" << std::setw(10) << fragStartPos << "\r\n";
clipHeaderBuilder << "EndFragment:" << std::setw(10) << fragEndPos << "\r\n";
clipHeaderBuilder << "StartSelection:" << std::setw(10) << fragStartPos << "\r\n";
clipHeaderBuilder << "EndSelection:" << std::setw(10) << fragEndPos << "\r\n";
return clipHeaderBuilder + htmlBuilder;
return clipHeaderBuilder.str() + htmlBuilder.str();
}
catch (...)
{
@@ -2286,36 +2272,25 @@ std::string TextBuffer::GenHTML(const CopyRequest& req,
}
// Routine Description:
// - Generates an RTF document from the selected region of the buffer
// - Generates an RTF document based on the passed in text and color data
// RTF 1.5 Spec: https://www.biblioscape.com/rtf15_spec.htm
// RTF 1.9.1 Spec: https://msopenspecs.azureedge.net/files/Archive_References/[MSFT-RTF].pdf
// Arguments:
// - req - the copy request having the bounds of the selected region and other related configuration flags.
// - rows - the text and color data we will format & encapsulate
// - backgroundColor - default background color for characters, also used in padding
// - fontHeightPoints - the unscaled font height
// - fontFaceName - the name of the font used
// - backgroundColor - default background color for characters, also used in padding
// - isIntenseBold - true if being intense is treated as being bold
// - GetAttributeColors - function to get the colors of the text attributes as they're rendered
// - htmlTitle - value used in title tag of html header. Used to name the application
// Return Value:
// - string containing the generated RTF. Empty if the copy request is invalid.
std::string TextBuffer::GenRTF(const CopyRequest& req,
const int fontHeightPoints,
const std::wstring_view fontFaceName,
const COLORREF backgroundColor,
const bool isIntenseBold,
std::function<std::tuple<COLORREF, COLORREF, COLORREF>(const TextAttribute&)> GetAttributeColors) const noexcept
// - string containing the generated RTF
std::string TextBuffer::GenRTF(const TextAndColor& rows, const int fontHeightPoints, const std::wstring_view fontFaceName, const COLORREF backgroundColor)
{
if (req.beg > req.end)
{
return {};
}
try
{
std::string rtfBuilder;
std::ostringstream rtfBuilder;
// start rtf
rtfBuilder += "{";
rtfBuilder << "{";
// Standard RTF header.
// This is similar to the header generated by WordPad.
@@ -2331,11 +2306,10 @@ std::string TextBuffer::GenRTF(const CopyRequest& req,
// Some features are blocked by default to maintain compatibility
// with older programs (Eg. Word 97-2003). `nouicompat` disables this
// behavior, and unblocks these features. See: Spec 1.9.1, Pg. 51.
rtfBuilder += "\\rtf1\\ansi\\ansicpg1252\\deff0\\nouicompat";
rtfBuilder << "\\rtf1\\ansi\\ansicpg1252\\deff0\\nouicompat";
// font table
// Brace escape: add an extra brace (of same kind) after a brace to escape it within the format string.
fmt::format_to(std::back_inserter(rtfBuilder), FMT_COMPILE("{{\\fonttbl{{\\f0\\fmodern\\fcharset0 {};}}}}"), til::u16u8(fontFaceName));
rtfBuilder << "{\\fonttbl{\\f0\\fmodern\\fcharset0 " << ConvertToA(CP_UTF8, fontFaceName) << ";}}";
// map to keep track of colors:
// keys are colors represented by COLORREF
@@ -2343,8 +2317,8 @@ std::string TextBuffer::GenRTF(const CopyRequest& req,
std::unordered_map<COLORREF, size_t> colorMap;
// RTF color table
std::string colorTableBuilder;
colorTableBuilder += "{\\colortbl ;";
std::ostringstream colorTableBuilder;
colorTableBuilder << "{\\colortbl ;";
const auto getColorTableIndex = [&](const COLORREF color) -> size_t {
// Exclude the 0 index for the default color, and start with 1.
@@ -2352,127 +2326,103 @@ std::string TextBuffer::GenRTF(const CopyRequest& req,
const auto [it, inserted] = colorMap.emplace(color, colorMap.size() + 1);
if (inserted)
{
const auto red = static_cast<int>(GetRValue(color));
const auto green = static_cast<int>(GetGValue(color));
const auto blue = static_cast<int>(GetBValue(color));
fmt::format_to(std::back_inserter(colorTableBuilder), FMT_COMPILE("\\red{}\\green{}\\blue{};"), red, green, blue);
colorTableBuilder << "\\red" << static_cast<int>(GetRValue(color))
<< "\\green" << static_cast<int>(GetGValue(color))
<< "\\blue" << static_cast<int>(GetBValue(color))
<< ";";
}
return it->second;
};
// content
std::string contentBuilder;
// \viewkindN: View mode of the document to be used. N=4 specifies that the document is in Normal view. (maybe unnecessary?)
// \ucN: Number of unicode fallback characters after each codepoint. (global)
contentBuilder += "\\viewkind4\\uc1";
std::ostringstream contentBuilder;
contentBuilder << "\\viewkind4\\uc4";
// paragraph styles
// \pard: paragraph description
// \slmultN: line-spacing multiple
// \fN: font to be used for the paragraph, where N is the font index in the font table
contentBuilder += "\\pard\\slmult1\\f0";
// \fs specifies font size in half-points i.e. \fs20 results in a font size
// of 10 pts. That's why, font size is multiplied by 2 here.
contentBuilder << "\\pard\\slmult1\\f0\\fs" << std::to_string(2 * fontHeightPoints)
// Set the background color for the page. But, the
// standard way (\cbN) to do this isn't supported in Word.
// However, the following control words sequence works
// in Word (and other RTF editors also) for applying the
// text background color. See: Spec 1.9.1, Pg. 23.
<< "\\chshdng0\\chcbpat" << getColorTableIndex(backgroundColor)
<< " ";
// \fsN: specifies font size in half-points. E.g. \fs20 results in a font
// size of 10 pts. That's why, font size is multiplied by 2 here.
fmt::format_to(std::back_inserter(contentBuilder), FMT_COMPILE("\\fs{}"), std::to_string(2 * fontHeightPoints));
// Set the background color for the page. But the standard way (\cbN) to do
// this isn't supported in Word. However, the following control words sequence
// works in Word (and other RTF editors also) for applying the text background
// color. See: Spec 1.9.1, Pg. 23.
fmt::format_to(std::back_inserter(contentBuilder), FMT_COMPILE("\\chshdng0\\chcbpat{}"), getColorTableIndex(backgroundColor));
for (auto iRow = req.beg.y; iRow <= req.end.y; ++iRow)
std::optional<COLORREF> fgColor = std::nullopt;
std::optional<COLORREF> bkColor = std::nullopt;
for (size_t row = 0; row < rows.text.size(); ++row)
{
const auto& row = GetRowByOffset(iRow);
const auto [rowBeg, rowEnd, addLineBreak] = _RowCopyHelper(req, iRow, row);
const auto rowBegU16 = gsl::narrow_cast<uint16_t>(rowBeg);
const auto rowEndU16 = gsl::narrow_cast<uint16_t>(rowEnd);
const auto runs = row.Attributes().slice(rowBegU16, rowEndU16).runs();
size_t startOffset = 0;
auto x = rowBegU16;
for (auto& [attr, length] : runs)
if (row != 0)
{
const auto nextX = gsl::narrow_cast<uint16_t>(x + length);
const auto [fg, bg, ul] = GetAttributeColors(attr);
const auto fgIdx = getColorTableIndex(fg);
const auto bgIdx = getColorTableIndex(bg);
const auto ulIdx = getColorTableIndex(ul);
const auto ulStyle = attr.GetUnderlineStyle();
// start an RTF group that can be closed later to restore the
// default attribute.
contentBuilder += "{";
fmt::format_to(std::back_inserter(contentBuilder), FMT_COMPILE("\\cf{}"), fgIdx);
fmt::format_to(std::back_inserter(contentBuilder), FMT_COMPILE("\\chshdng0\\chcbpat{}"), bgIdx);
if (isIntenseBold && attr.IsIntense())
{
contentBuilder += "\\b";
}
if (attr.IsItalic())
{
contentBuilder += "\\i";
}
if (attr.IsCrossedOut())
{
contentBuilder += "\\strike";
}
switch (ulStyle)
{
case UnderlineStyle::NoUnderline:
break;
case UnderlineStyle::DoublyUnderlined:
fmt::format_to(std::back_inserter(contentBuilder), FMT_COMPILE("\\uldb\\ulc{}"), ulIdx);
break;
case UnderlineStyle::CurlyUnderlined:
fmt::format_to(std::back_inserter(contentBuilder), FMT_COMPILE("\\ulwave\\ulc{}"), ulIdx);
break;
case UnderlineStyle::DottedUnderlined:
fmt::format_to(std::back_inserter(contentBuilder), FMT_COMPILE("\\uld\\ulc{}"), ulIdx);
break;
case UnderlineStyle::DashedUnderlined:
fmt::format_to(std::back_inserter(contentBuilder), FMT_COMPILE("\\uldash\\ulc{}"), ulIdx);
break;
case UnderlineStyle::SinglyUnderlined:
default:
fmt::format_to(std::back_inserter(contentBuilder), FMT_COMPILE("\\ul\\ulc{}"), ulIdx);
break;
}
// RTF commands and the text data must be separated by a space.
// Otherwise, if the text begins with a space then that space will
// be interpreted as part of the last command, and will be lost.
contentBuilder += " ";
const auto unescapedText = row.GetText(x, nextX); // including character at nextX
_AppendRTFText(contentBuilder, unescapedText);
contentBuilder += "}"; // close RTF group
// advance to next run of text
x = nextX;
contentBuilder << "\\line "; // new line
}
// never add line break to the last row.
if (addLineBreak && iRow < req.end.y)
for (size_t col = 0; col < rows.text.at(row).length(); ++col)
{
contentBuilder += "\\line";
const auto writeAccumulatedChars = [&](bool includeCurrent) {
if (col >= startOffset)
{
const auto text = std::wstring_view{ rows.text.at(row) }.substr(startOffset, col - startOffset + includeCurrent);
_AppendRTFText(contentBuilder, text);
startOffset = col;
}
};
if (rows.text.at(row).at(col) == '\r' || rows.text.at(row).at(col) == '\n')
{
// do not include \r nor \n as they don't have color attributes.
// For line break use \line instead.
writeAccumulatedChars(false);
break;
}
auto colorChanged = false;
if (!fgColor.has_value() || rows.FgAttr.at(row).at(col) != fgColor.value())
{
fgColor = rows.FgAttr.at(row).at(col);
colorChanged = true;
}
if (!bkColor.has_value() || rows.BkAttr.at(row).at(col) != bkColor.value())
{
bkColor = rows.BkAttr.at(row).at(col);
colorChanged = true;
}
if (colorChanged)
{
writeAccumulatedChars(false);
contentBuilder << "\\chshdng0\\chcbpat" << getColorTableIndex(bkColor.value())
<< "\\cf" << getColorTableIndex(fgColor.value())
<< " ";
}
// if this is the last character in the row, flush the whole row
if (col == rows.text.at(row).length() - 1)
{
writeAccumulatedChars(true);
}
}
}
// end colortbl
colorTableBuilder << "}";
// add color table to the final RTF
rtfBuilder += colorTableBuilder + "}";
rtfBuilder << colorTableBuilder.str();
// add the text content to the final RTF
rtfBuilder += contentBuilder + "}";
rtfBuilder << contentBuilder.str();
return rtfBuilder;
// end rtf
rtfBuilder << "}";
return rtfBuilder.str();
}
catch (...)
{
@@ -2481,7 +2431,7 @@ std::string TextBuffer::GenRTF(const CopyRequest& req,
}
}
void TextBuffer::_AppendRTFText(std::string& contentBuilder, const std::wstring_view& text)
void TextBuffer::_AppendRTFText(std::ostringstream& contentBuilder, const std::wstring_view& text)
{
for (const auto codeUnit : text)
{
@@ -2492,18 +2442,16 @@ void TextBuffer::_AppendRTFText(std::string& contentBuilder, const std::wstring_
case L'\\':
case L'{':
case L'}':
contentBuilder += "\\";
[[fallthrough]];
contentBuilder << "\\" << gsl::narrow<char>(codeUnit);
break;
default:
contentBuilder += gsl::narrow_cast<char>(codeUnit);
contentBuilder << gsl::narrow<char>(codeUnit);
}
}
else
{
// Windows uses unsigned wchar_t - RTF uses signed ones.
// '?' is the fallback ascii character.
const auto codeUnitRTFStr = std::to_string(til::bit_cast<int16_t>(codeUnit));
fmt::format_to(std::back_inserter(contentBuilder), FMT_COMPILE("\\u{}?"), codeUnitRTFStr);
contentBuilder << "\\u" << std::to_string(til::bit_cast<int16_t>(codeUnit)) << "?";
}
}
}

View File

@@ -194,7 +194,6 @@ public:
til::point BufferToScreenPosition(const til::point position) const;
void Reset() noexcept;
void ClearScrollback(const til::CoordType start, const til::CoordType height);
void ResizeTraditional(const til::size newSize);
@@ -230,94 +229,33 @@ public:
std::wstring GetCustomIdFromId(uint16_t id) const;
void CopyHyperlinkMaps(const TextBuffer& OtherBuffer);
class TextAndColor
{
public:
std::vector<std::wstring> text;
std::vector<std::vector<COLORREF>> FgAttr;
std::vector<std::vector<COLORREF>> BkAttr;
};
size_t SpanLength(const til::point coordStart, const til::point coordEnd) const;
const TextAndColor GetText(const bool includeCRLF,
const bool trimTrailingWhitespace,
const std::vector<til::inclusive_rect>& textRects,
std::function<std::pair<COLORREF, COLORREF>(const TextAttribute&)> GetAttributeColors = nullptr,
const bool formatWrappedRows = false) const;
std::wstring GetPlainText(const til::point& start, const til::point& end) const;
struct CopyRequest
{
// beg and end coordinates are inclusive
til::point beg;
til::point end;
static std::string GenHTML(const TextAndColor& rows,
const int fontHeightPoints,
const std::wstring_view fontFaceName,
const COLORREF backgroundColor);
til::CoordType minX;
til::CoordType maxX;
bool blockSelection = false;
bool trimTrailingWhitespace = true;
bool includeLineBreak = true;
bool formatWrappedRows = false;
// whether beg, end coordinates are in buffer coordinates or screen coordinates
bool bufferCoordinates = false;
CopyRequest() = default;
constexpr CopyRequest(const TextBuffer& buffer, const til::point& beg, const til::point& end, const bool blockSelection, const bool includeLineBreak, const bool trimTrailingWhitespace, const bool formatWrappedRows, const bool bufferCoordinates = false) noexcept :
beg{ std::max(beg, til::point{ 0, 0 }) },
end{ std::min(end, til::point{ buffer._width - 1, buffer._height - 1 }) },
minX{ std::min(this->beg.x, this->end.x) },
maxX{ std::max(this->beg.x, this->end.x) },
blockSelection{ blockSelection },
includeLineBreak{ includeLineBreak },
trimTrailingWhitespace{ trimTrailingWhitespace },
formatWrappedRows{ formatWrappedRows },
bufferCoordinates{ bufferCoordinates }
{
}
static CopyRequest FromConfig(const TextBuffer& buffer,
const til::point& beg,
const til::point& end,
const bool singleLine,
const bool blockSelection,
const bool trimBlockSelection,
const bool bufferCoordinates = false) noexcept
{
return {
buffer,
beg,
end,
blockSelection,
/* includeLineBreak */
// - SingleLine mode collapses all rows into one line, unless we're in
// block selection mode.
// - Block selection should preserve the visual structure by including
// line breaks on all rows (together with `formatWrappedRows`).
// (Selects like a box, pastes like a box)
!singleLine || blockSelection,
/* trimTrailingWhitespace */
// Trim trailing whitespace if we're not in single line mode and — either
// we're not in block selection mode or, we're in block selection mode and
// trimming is allowed.
!singleLine && (!blockSelection || trimBlockSelection),
/* formatWrappedRows */
// In block selection, we should apply formatting to wrapped rows as well.
// (Otherwise, they're only applied to non-wrapped rows.)
blockSelection,
bufferCoordinates
};
}
};
std::wstring GetPlainText(const CopyRequest& req) const;
std::string GenHTML(const CopyRequest& req,
const int fontHeightPoints,
const std::wstring_view fontFaceName,
const COLORREF backgroundColor,
const bool isIntenseBold,
std::function<std::tuple<COLORREF, COLORREF, COLORREF>(const TextAttribute&)> GetAttributeColors) const noexcept;
std::string GenRTF(const CopyRequest& req,
const int fontHeightPoints,
const std::wstring_view fontFaceName,
const COLORREF backgroundColor,
const bool isIntenseBold,
std::function<std::tuple<COLORREF, COLORREF, COLORREF>(const TextAttribute&)> GetAttributeColors) const noexcept;
static std::string GenRTF(const TextAndColor& rows,
const int fontHeightPoints,
const std::wstring_view fontFaceName,
const COLORREF backgroundColor);
struct PositionInformation
{
@@ -365,9 +303,8 @@ private:
til::point _GetWordEndForSelection(const til::point target, const std::wstring_view wordDelimiters) const;
void _PruneHyperlinks();
void _trimMarksOutsideBuffer();
std::tuple<til::CoordType, til::CoordType, bool> _RowCopyHelper(const CopyRequest& req, const til::CoordType iRow, const ROW& row) const;
static void _AppendRTFText(std::string& contentBuilder, const std::wstring_view& text);
static void _AppendRTFText(std::ostringstream& contentBuilder, const std::wstring_view& text);
Microsoft::Console::Render::Renderer& _renderer;

View File

@@ -39,6 +39,7 @@
<Dependencies>
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.19041.0" MaxVersionTested="10.0.22621.0" />
<TargetDeviceFamily Name="Windows.DesktopServer" MinVersion="10.0.19041.0" MaxVersionTested="10.0.22621.0" />
</Dependencies>
<Resources>

View File

@@ -40,6 +40,7 @@
<Dependencies>
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.19041.0" MaxVersionTested="10.0.22621.0" />
<TargetDeviceFamily Name="Windows.DesktopServer" MinVersion="10.0.19041.0" MaxVersionTested="10.0.22621.0" />
</Dependencies>
<Resources>

View File

@@ -40,6 +40,7 @@
<Dependencies>
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.19041.0" MaxVersionTested="10.0.22621.0" />
<TargetDeviceFamily Name="Windows.DesktopServer" MinVersion="10.0.19041.0" MaxVersionTested="10.0.22621.0" />
</Dependencies>
<Resources>

View File

@@ -68,8 +68,6 @@ Author(s):
// Manually include til after we include Windows.Foundation to give it winrt superpowers
#include "til.h"
#include <SafeDispatcherTimer.h>
// Common includes for most tests:
#include "../../inc/conattrs.hpp"
#include "../../types/inc/utils.hpp"

View File

@@ -1065,7 +1065,10 @@ namespace winrt::TerminalApp::implementation
{
if (termControl.HasSelection())
{
std::wstring searchText{ termControl.SelectedText(true) };
const auto selections{ termControl.SelectedText(true) };
// concatenate the selection into a single line
auto searchText = std::accumulate(selections.begin(), selections.end(), std::wstring());
// make it compact by replacing consecutive whitespaces with a single space
searchText = std::regex_replace(searchText, std::wregex(LR"(\s+)"), L" ");

View File

@@ -124,7 +124,8 @@ namespace winrt::TerminalApp::implementation
return appLogic->GetSettings();
}
AppLogic::AppLogic()
AppLogic::AppLogic() :
_reloadState{ std::chrono::milliseconds(100), []() { ApplicationState::SharedInstance().Reload(); } }
{
// For your own sanity, it's better to do setup outside the ctor.
// If you do any setup in the ctor that ends up throwing an exception,
@@ -326,6 +327,10 @@ namespace winrt::TerminalApp::implementation
{
_reloadSettings->Run();
}
else if (ApplicationState::SharedInstance().IsStatePath(modifiedBasename))
{
_reloadState();
}
});
}

View File

@@ -91,6 +91,7 @@ namespace winrt::TerminalApp::implementation
::TerminalApp::AppCommandlineArgs _settingsAppArgs;
std::shared_ptr<ThrottledFuncTrailing<>> _reloadSettings;
til::throttled_func_trailing<> _reloadState;
std::vector<Microsoft::Terminal::Settings::Model::SettingsLoadWarnings> _warnings{};

View File

@@ -1,4 +1,6 @@
#include "pch.h"
#include "ColorHelper.h"
#include <limits>
using namespace winrt::TerminalApp;

View File

@@ -1,6 +1,7 @@
#pragma once
#include "pch.h"
#include <winrt/Windows.UI.h>
#include <winrt/windows.ui.core.h>
namespace winrt::TerminalApp
{

View File

@@ -50,7 +50,6 @@ namespace winrt::Microsoft::TerminalApp::implementation
void TerminalOutput(const winrt::event_token& token) noexcept { _wrappedConnection.TerminalOutput(token); };
winrt::event_token StateChanged(const TypedEventHandler<ITerminalConnection, IInspectable>& handler) { return _wrappedConnection.StateChanged(handler); };
void StateChanged(const winrt::event_token& token) noexcept { _wrappedConnection.StateChanged(token); };
winrt::guid SessionId() const noexcept { return {}; }
ConnectionState State() const noexcept { return _wrappedConnection.State(); }
private:
@@ -99,15 +98,6 @@ namespace winrt::Microsoft::TerminalApp::implementation
_wrappedConnection = nullptr;
}
guid DebugTapConnection::SessionId() const noexcept
{
if (const auto c = _wrappedConnection.get())
{
return c.SessionId();
}
return {};
}
ConnectionState DebugTapConnection::State() const noexcept
{
if (auto strongConnection{ _wrappedConnection.get() })

View File

@@ -19,8 +19,6 @@ namespace winrt::Microsoft::TerminalApp::implementation
void WriteInput(const hstring& data);
void Resize(uint32_t rows, uint32_t columns);
void Close();
winrt::guid SessionId() const noexcept;
winrt::Microsoft::Terminal::TerminalConnection::ConnectionState State() const noexcept;
void SetInputTap(const Microsoft::Terminal::TerminalConnection::ITerminalConnection& inputTap);

View File

@@ -237,9 +237,7 @@
</ClCompile>
<ClCompile Include="Pane.cpp" />
<ClCompile Include="Pane.LayoutSizeNode.cpp" />
<ClCompile Include="ColorHelper.cpp">
<PrecompiledHeader>NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="ColorHelper.cpp" />
<ClCompile Include="DebugTapConnection.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>

View File

@@ -1210,7 +1210,7 @@ namespace winrt::TerminalApp::implementation
TerminalConnection::ITerminalConnection connection{ nullptr };
auto connectionType = profile.ConnectionType();
Windows::Foundation::Collections::ValueSet valueSet;
winrt::guid sessionGuid{};
if (connectionType == TerminalConnection::AzureConnection::ConnectionType() &&
TerminalConnection::AzureConnection::IsAzureConnectionAvailable())
@@ -1226,16 +1226,23 @@ namespace winrt::TerminalApp::implementation
connection = TerminalConnection::ConptyConnection{};
}
valueSet = TerminalConnection::ConptyConnection::CreateSettings(azBridgePath.native(),
L".",
L"Azure",
false,
L"",
nullptr,
settings.InitialRows(),
settings.InitialCols(),
winrt::guid(),
profile.Guid());
auto valueSet = TerminalConnection::ConptyConnection::CreateSettings(azBridgePath.native(),
L".",
L"Azure",
false,
L"",
nullptr,
settings.InitialRows(),
settings.InitialCols(),
winrt::guid(),
profile.Guid());
if constexpr (Feature_VtPassthroughMode::IsEnabled())
{
valueSet.Insert(L"passthroughMode", Windows::Foundation::PropertyValue::CreateBoolean(settings.VtPassthrough()));
}
connection.Initialize(valueSet);
}
else
@@ -1260,30 +1267,30 @@ namespace winrt::TerminalApp::implementation
// process until later, on another thread, after we've already
// restored the CWD to its original value.
auto newWorkingDirectory{ _evaluatePathForCwd(settings.StartingDirectory()) };
connection = TerminalConnection::ConptyConnection{};
valueSet = TerminalConnection::ConptyConnection::CreateSettings(settings.Commandline(),
newWorkingDirectory,
settings.StartingTitle(),
settings.ReloadEnvironmentVariables(),
_WindowProperties.VirtualEnvVars(),
environment,
settings.InitialRows(),
settings.InitialCols(),
winrt::guid(),
profile.Guid());
auto conhostConn = TerminalConnection::ConptyConnection();
auto valueSet = TerminalConnection::ConptyConnection::CreateSettings(settings.Commandline(),
newWorkingDirectory,
settings.StartingTitle(),
settings.ReloadEnvironmentVariables(),
_WindowProperties.VirtualEnvVars(),
environment,
settings.InitialRows(),
settings.InitialCols(),
winrt::guid(),
profile.Guid());
valueSet.Insert(L"passthroughMode", Windows::Foundation::PropertyValue::CreateBoolean(settings.VtPassthrough()));
if (inheritCursor)
{
valueSet.Insert(L"inheritCursor", Windows::Foundation::PropertyValue::CreateBoolean(true));
}
}
if constexpr (Feature_VtPassthroughMode::IsEnabled())
{
valueSet.Insert(L"passthroughMode", Windows::Foundation::PropertyValue::CreateBoolean(settings.VtPassthrough()));
}
conhostConn.Initialize(valueSet);
connection.Initialize(valueSet);
sessionGuid = conhostConn.Guid();
connection = conhostConn;
}
TraceLoggingWrite(
g_hTerminalAppProvider,
@@ -1291,7 +1298,7 @@ namespace winrt::TerminalApp::implementation
TraceLoggingDescription("Event emitted upon the creation of a connection"),
TraceLoggingGuid(connectionType, "ConnectionTypeGuid", "The type of the connection"),
TraceLoggingGuid(profile.Guid(), "ProfileGuid", "The profile's GUID"),
TraceLoggingGuid(connection.SessionId(), "SessionGuid", "The WT_SESSION's GUID"),
TraceLoggingGuid(sessionGuid, "SessionGuid", "The WT_SESSION's GUID"),
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage));
@@ -2586,9 +2593,12 @@ namespace winrt::TerminalApp::implementation
auto dataPack = DataPackage();
dataPack.RequestedOperation(DataPackageOperation::Copy);
const auto copyFormats = copiedData.Formats() != nullptr ?
copiedData.Formats().Value() :
static_cast<CopyFormat>(0);
// The EventArgs.Formats() is an override for the global setting "copyFormatting"
// iff it is set
auto useGlobal = copiedData.Formats() == nullptr;
auto copyFormats = useGlobal ?
_settings.GlobalSettings().CopyFormatting() :
copiedData.Formats().Value();
// copy text to dataPack
dataPack.SetText(copiedData.Text());
@@ -2621,75 +2631,6 @@ namespace winrt::TerminalApp::implementation
CATCH_LOG();
}
static wil::unique_close_clipboard_call _openClipboard(HWND hwnd)
{
bool success = false;
// OpenClipboard may fail to acquire the internal lock --> retry.
for (DWORD sleep = 10;; sleep *= 2)
{
if (OpenClipboard(hwnd))
{
success = true;
break;
}
// 10 iterations
if (sleep > 10000)
{
break;
}
Sleep(sleep);
}
return wil::unique_close_clipboard_call{ success };
}
static winrt::hstring _extractClipboard()
{
// This handles most cases of pasting text as the OS converts most formats to CF_UNICODETEXT automatically.
if (const auto handle = GetClipboardData(CF_UNICODETEXT))
{
const wil::unique_hglobal_locked lock{ handle };
const auto str = static_cast<const wchar_t*>(lock.get());
if (!str)
{
return {};
}
const auto maxLen = GlobalSize(handle) / sizeof(wchar_t);
const auto len = wcsnlen(str, maxLen);
return winrt::hstring{ str, gsl::narrow_cast<uint32_t>(len) };
}
// We get CF_HDROP when a user copied a file with Ctrl+C in Explorer and pastes that into the terminal (among others).
if (const auto handle = GetClipboardData(CF_HDROP))
{
const wil::unique_hglobal_locked lock{ handle };
const auto drop = static_cast<HDROP>(lock.get());
if (!drop)
{
return {};
}
const auto cap = DragQueryFileW(drop, 0, nullptr, 0);
if (cap == 0)
{
return {};
}
auto buffer = winrt::impl::hstring_builder{ cap };
const auto len = DragQueryFileW(drop, 0, buffer.data(), cap + 1);
if (len == 0)
{
return {};
}
return buffer.to_hstring();
}
return {};
}
// Function Description:
// - This function is called when the `TermControl` requests that we send
// it the clipboard's content.
@@ -2709,14 +2650,53 @@ namespace winrt::TerminalApp::implementation
const auto weakThis = get_weak();
const auto dispatcher = Dispatcher();
const auto globalSettings = _settings.GlobalSettings();
winrt::hstring text;
// GetClipboardData might block for up to 30s for delay-rendered contents.
co_await winrt::resume_background();
winrt::hstring text;
if (const auto clipboard = _openClipboard(nullptr))
{
text = _extractClipboard();
// According to various reports on the internet, OpenClipboard might
// fail to acquire the internal lock, for instance due to rdpclip.exe.
for (int attempts = 1;;)
{
if (OpenClipboard(nullptr))
{
break;
}
if (attempts > 5)
{
co_return;
}
attempts++;
Sleep(10 * attempts);
}
const auto clipboardCleanup = wil::scope_exit([]() {
CloseClipboard();
});
const auto data = GetClipboardData(CF_UNICODETEXT);
if (!data)
{
co_return;
}
const auto str = static_cast<const wchar_t*>(GlobalLock(data));
if (!str)
{
co_return;
}
const auto dataCleanup = wil::scope_exit([&]() {
GlobalUnlock(data);
});
const auto maxLength = GlobalSize(data) / sizeof(wchar_t);
const auto length = wcsnlen(str, maxLength);
text = winrt::hstring{ str, gsl::narrow_cast<uint32_t>(length) };
}
if (globalSettings.TrimPaste())

View File

@@ -121,7 +121,12 @@ namespace winrt::TerminalApp::implementation
void TerminalTab::_BellIndicatorTimerTick(const Windows::Foundation::IInspectable& /*sender*/, const Windows::Foundation::IInspectable& /*e*/)
{
ShowBellIndicator(false);
_bellIndicatorTimer.Stop();
// Just do a sanity check that the timer still exists before we stop it
if (_bellIndicatorTimer.has_value())
{
_bellIndicatorTimer->Stop();
_bellIndicatorTimer = std::nullopt;
}
}
// Method Description:
@@ -351,13 +356,14 @@ namespace winrt::TerminalApp::implementation
{
ASSERT_UI_THREAD();
if (!_bellIndicatorTimer)
if (!_bellIndicatorTimer.has_value())
{
_bellIndicatorTimer.Interval(std::chrono::milliseconds(2000));
_bellIndicatorTimer.Tick({ get_weak(), &TerminalTab::_BellIndicatorTimerTick });
DispatcherTimer bellIndicatorTimer;
bellIndicatorTimer.Interval(std::chrono::milliseconds(2000));
bellIndicatorTimer.Tick({ get_weak(), &TerminalTab::_BellIndicatorTimerTick });
bellIndicatorTimer.Start();
_bellIndicatorTimer.emplace(std::move(bellIndicatorTimer));
}
_bellIndicatorTimer.Start();
}
// Method Description:

View File

@@ -152,7 +152,7 @@ namespace winrt::TerminalApp::implementation
void _Setup();
SafeDispatcherTimer _bellIndicatorTimer;
std::optional<Windows::UI::Xaml::DispatcherTimer> _bellIndicatorTimer;
void _BellIndicatorTimerTick(const Windows::Foundation::IInspectable& sender, const Windows::Foundation::IInspectable& e);
void _MakeTabViewItem() override;

View File

@@ -34,5 +34,5 @@ public:
private:
winrt::Microsoft::UI::Xaml::Controls::TeachingTip _tip;
SafeDispatcherTimer _timer;
winrt::Windows::UI::Xaml::DispatcherTimer _timer;
};

View File

@@ -83,8 +83,6 @@ TRACELOGGING_DECLARE_PROVIDER(g_hTerminalAppProvider);
// Manually include til after we include Windows.Foundation to give it winrt superpowers
#include "til.h"
#include <SafeDispatcherTimer.h>
#include <cppwinrt_utils.h>
#include <wil/cppwinrt_helpers.h> // must go after the CoreDispatcher type is defined

View File

@@ -77,14 +77,8 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
{
if (settings)
{
_initialRows = unbox_prop_or<uint32_t>(settings, L"initialRows", _initialRows);
_initialCols = unbox_prop_or<uint32_t>(settings, L"initialCols", _initialCols);
_sessionId = unbox_prop_or<guid>(settings, L"sessionId", _sessionId);
}
if (_sessionId == guid{})
{
_sessionId = Utils::CreateGuid();
_initialRows = gsl::narrow<til::CoordType>(winrt::unbox_value_or<uint32_t>(settings.TryLookup(L"initialRows").try_as<Windows::Foundation::IPropertyValue>(), _initialRows));
_initialCols = gsl::narrow<til::CoordType>(winrt::unbox_value_or<uint32_t>(settings.TryLookup(L"initialCols").try_as<Windows::Foundation::IPropertyValue>(), _initialCols));
}
}

View File

@@ -8,12 +8,12 @@
#include <mutex>
#include <condition_variable>
#include "BaseTerminalConnection.h"
#include "ConnectionStateHolder.h"
#include "AzureClient.h"
namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
{
struct AzureConnection : AzureConnectionT<AzureConnection>, BaseTerminalConnection<AzureConnection>
struct AzureConnection : AzureConnectionT<AzureConnection>, ConnectionStateHolder<AzureConnection>
{
static winrt::guid ConnectionType() noexcept;
static bool IsAzureConnectionAvailable() noexcept;

View File

@@ -4,28 +4,13 @@
namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
{
template<typename T>
struct BaseTerminalConnection
struct ConnectionStateHolder
{
public:
winrt::guid SessionId() const noexcept
{
return _sessionId;
}
ConnectionState State() const noexcept
{
return _connectionState;
}
ConnectionState State() const noexcept { return _connectionState; }
TYPED_EVENT(StateChanged, ITerminalConnection, winrt::Windows::Foundation::IInspectable);
protected:
template<typename U>
U unbox_prop_or(const Windows::Foundation::Collections::ValueSet& blob, std::wstring_view key, U defaultValue)
{
return winrt::unbox_value_or<U>(blob.TryLookup(key).try_as<Windows::Foundation::IPropertyValue>(), defaultValue);
}
#pragma warning(push)
#pragma warning(disable : 26447) // Analyzer is still upset about noexcepts throwing even with function level try.
// Method Description:
@@ -101,8 +86,6 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
return _isStateOneOf(ConnectionState::Connected);
}
winrt::guid _sessionId{};
private:
std::atomic<ConnectionState> _connectionState{ ConnectionState::NotConnected };
mutable std::mutex _stateMutex;

View File

@@ -85,12 +85,18 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
auto environment = _initialEnv;
{
// Ensure every connection has the unique identifier in the environment.
// Convert connection Guid to string and ignore the enclosing '{}'.
environment.as_map().insert_or_assign(L"WT_SESSION", Utils::GuidToPlainString(_sessionId));
auto wsGuid{ Utils::GuidToString(_guid) };
wsGuid.pop_back();
const auto guidSubStr = std::wstring_view{ wsGuid }.substr(1);
// Ensure every connection has the unique identifier in the environment.
environment.as_map().insert_or_assign(L"WT_SESSION", guidSubStr.data());
// The profile Guid does include the enclosing '{}'
environment.as_map().insert_or_assign(L"WT_PROFILE_ID", Utils::GuidToString(_profileGuid));
const auto profileGuid{ Utils::GuidToString(_profileGuid) };
environment.as_map().insert_or_assign(L"WT_PROFILE_ID", profileGuid.data());
// WSLENV is a colon-delimited list of environment variables (+flags) that should appear inside WSL
// https://devblogs.microsoft.com/commandline/share-environment-vars-between-wsl-and-windows/
@@ -165,7 +171,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
g_hTerminalConnectionProvider,
"ConPtyConnected",
TraceLoggingDescription("Event emitted when ConPTY connection is started"),
TraceLoggingGuid(_sessionId, "SessionGuid", "The WT_SESSION's GUID"),
TraceLoggingGuid(_guid, "SessionGuid", "The WT_SESSION's GUID"),
TraceLoggingWideString(_clientName.c_str(), "Client", "The attached client process"),
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage));
@@ -183,6 +189,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
TERMINAL_STARTUP_INFO startupInfo) :
_rows{ 25 },
_cols{ 80 },
_guid{ Utils::CreateGuid() },
_inPipe{ hIn },
_outPipe{ hOut }
{
@@ -242,6 +249,12 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
return vs;
}
template<typename T>
T unbox_prop_or(const Windows::Foundation::Collections::ValueSet& blob, std::wstring_view key, T defaultValue)
{
return winrt::unbox_value_or<T>(blob.TryLookup(key).try_as<Windows::Foundation::IPropertyValue>(), defaultValue);
}
void ConptyConnection::Initialize(const Windows::Foundation::Collections::ValueSet& settings)
{
if (settings)
@@ -255,7 +268,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
_startingTitle = unbox_prop_or<winrt::hstring>(settings, L"startingTitle", _startingTitle);
_rows = unbox_prop_or<uint32_t>(settings, L"initialRows", _rows);
_cols = unbox_prop_or<uint32_t>(settings, L"initialCols", _cols);
_sessionId = unbox_prop_or<winrt::guid>(settings, L"sessionId", _sessionId);
_guid = unbox_prop_or<winrt::guid>(settings, L"guid", _guid);
_environment = settings.TryLookup(L"environment").try_as<Windows::Foundation::Collections::ValueSet>();
if constexpr (Feature_VtPassthroughMode::IsEnabled())
{
@@ -286,14 +299,19 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
_initialEnv = til::env::from_current_environment();
}
}
}
if (_sessionId == guid{})
{
_sessionId = Utils::CreateGuid();
if (_guid == guid{})
{
_guid = Utils::CreateGuid();
}
}
}
winrt::guid ConptyConnection::Guid() const noexcept
{
return _guid;
}
winrt::hstring ConptyConnection::Commandline() const
{
return _commandline;
@@ -364,7 +382,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
g_hTerminalConnectionProvider,
"ConPtyConnectedToDefterm",
TraceLoggingDescription("Event emitted when ConPTY connection is started, for a defterm session"),
TraceLoggingGuid(_sessionId, "SessionGuid", "The WT_SESSION's GUID"),
TraceLoggingGuid(_guid, "SessionGuid", "The WT_SESSION's GUID"),
TraceLoggingWideString(_clientName.c_str(), "Client", "The attached client process"),
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage));
@@ -668,7 +686,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
TraceLoggingWrite(g_hTerminalConnectionProvider,
"ReceivedFirstByte",
TraceLoggingDescription("An event emitted when the connection receives the first byte"),
TraceLoggingGuid(_sessionId, "SessionGuid", "The WT_SESSION's GUID"),
TraceLoggingGuid(_guid, "SessionGuid", "The WT_SESSION's GUID"),
TraceLoggingFloat64(delta.count(), "Duration"),
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance));

View File

@@ -4,14 +4,14 @@
#pragma once
#include "ConptyConnection.g.h"
#include "BaseTerminalConnection.h"
#include "ConnectionStateHolder.h"
#include "ITerminalHandoff.h"
#include <til/env.h>
namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
{
struct ConptyConnection : ConptyConnectionT<ConptyConnection>, BaseTerminalConnection<ConptyConnection>
struct ConptyConnection : ConptyConnectionT<ConptyConnection>, ConnectionStateHolder<ConptyConnection>
{
ConptyConnection(const HANDLE hSig,
const HANDLE hIn,
@@ -36,6 +36,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
void ReparentWindow(const uint64_t newParent);
winrt::guid Guid() const noexcept;
winrt::hstring Commandline() const;
winrt::hstring StartingTitle() const;
WORD ShowWindow() const noexcept;
@@ -76,6 +77,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
hstring _startingTitle{};
bool _initialVisibility{ true };
Windows::Foundation::Collections::ValueSet _environment{ nullptr };
guid _guid{}; // A unique session identifier for connected client
hstring _clientName{}; // The name of the process hosted by this ConPTY connection (as of launch).
bool _receivedFirstByte{ false };

View File

@@ -10,6 +10,7 @@ namespace Microsoft.Terminal.TerminalConnection
[default_interface] runtimeclass ConptyConnection : ITerminalConnection
{
ConptyConnection();
Guid Guid { get; };
String Commandline { get; };
String StartingTitle { get; };
UInt16 ShowWindow { get; };

View File

@@ -18,7 +18,6 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
void Initialize(const Windows::Foundation::Collections::ValueSet& /*settings*/) const noexcept {};
winrt::guid SessionId() const noexcept { return {}; }
ConnectionState State() const noexcept { return ConnectionState::Connected; }
WINRT_CALLBACK(TerminalOutput, TerminalOutputHandler);

View File

@@ -25,9 +25,8 @@ namespace Microsoft.Terminal.TerminalConnection
void Close();
event TerminalOutputHandler TerminalOutput;
event Windows.Foundation.TypedEventHandler<ITerminalConnection, Object> StateChanged;
Guid SessionId { get; };
event Windows.Foundation.TypedEventHandler<ITerminalConnection, Object> StateChanged;
ConnectionState State { get; };
};
}

View File

@@ -17,7 +17,6 @@
<Import Project="$(OpenConsoleDir)src\cppwinrt.build.pre.props" />
<ItemGroup>
<ClInclude Include="AzureClientID.h" />
<ClInclude Include="BaseTerminalConnection.h" />
<ClInclude Include="ConnectionInformation.h">
<DependentUpon>ConnectionInformation.idl</DependentUpon>
</ClInclude>

View File

@@ -26,7 +26,6 @@
<ClInclude Include="AzureConnection.h" />
<ClInclude Include="AzureClientID.h" />
<ClInclude Include="CTerminalHandoff.h" />
<ClInclude Include="BaseTerminalConnection.h" />
</ItemGroup>
<ItemGroup>
<Midl Include="ITerminalConnection.idl" />
@@ -35,9 +34,11 @@
<Midl Include="ConptyConnection.idl" />
<Midl Include="ConnectionInformation.idl" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<Natvis Include="$(SolutionDir)tools\ConsoleTypes.natvis" />
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" />
</ItemGroup>
<ItemGroup>
<PRIResource Include="Resources\en-US\Resources.resw" />

View File

@@ -17,6 +17,7 @@
#include "EventArgs.h"
#include "../../buffer/out/search.h"
#include "../../renderer/atlas/AtlasEngine.h"
#include "../../renderer/dx/DxRenderer.hpp"
#include "ControlCore.g.cpp"
#include "SelectionColor.g.cpp"
@@ -334,7 +335,15 @@ namespace winrt::Microsoft::Terminal::Control::implementation
return false;
}
_renderEngine = std::make_unique<::Microsoft::Console::Render::AtlasEngine>();
if (_settings->UseAtlasEngine())
{
_renderEngine = std::make_unique<::Microsoft::Console::Render::AtlasEngine>();
}
else
{
_renderEngine = std::make_unique<::Microsoft::Console::Render::DxEngine>();
}
_renderer->AddRenderEngine(_renderEngine.get());
// Initialize our font with the renderer
@@ -350,7 +359,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
const auto viewInPixels = Viewport::FromDimensions({ 0, 0 }, windowSize);
LOG_IF_FAILED(_renderEngine->SetWindowSize({ viewInPixels.Width(), viewInPixels.Height() }));
// Update AtlasEngine's SelectionBackground
// Update DxEngine's SelectionBackground
_renderEngine->SetSelectionBackground(til::color{ _settings->SelectionBackground() });
const auto vp = _renderEngine->GetViewportInCharacters(viewInPixels);
@@ -906,10 +915,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// Update the terminal core with its new Core settings
_terminal->UpdateAppearance(*newAppearance);
// Update AtlasEngine settings under the lock
// Update DxEngine settings under the lock
if (_renderEngine)
{
// Update AtlasEngine settings under the lock
// Update DxEngine settings under the lock
_renderEngine->SetSelectionBackground(til::color{ newAppearance->SelectionBackground() });
_renderEngine->SetRetroTerminalEffect(newAppearance->RetroTerminalEffect());
_renderEngine->SetPixelShaderPath(newAppearance->PixelShaderPath());
@@ -945,7 +954,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
void ControlCore::_updateAntiAliasingMode()
{
D2D1_TEXT_ANTIALIAS_MODE mode;
// Update AtlasEngine's AntialiasingMode
// Update DxEngine's AntialiasingMode
switch (_settings->AntialiasingMode())
{
case TextAntialiasingMode::Cleartype:
@@ -1241,23 +1250,44 @@ namespace winrt::Microsoft::Terminal::Control::implementation
return false;
}
// use action's copyFormatting if it's present, else fallback to globally
// set copyFormatting.
const auto copyFormats = formats != nullptr ? formats.Value() : _settings->CopyFormatting();
const auto copyHtml = WI_IsFlagSet(copyFormats, CopyFormat::HTML);
const auto copyRtf = WI_IsFlagSet(copyFormats, CopyFormat::RTF);
// extract text from buffer
// RetrieveSelectedTextFromBuffer will lock while it's reading
const auto& [textData, htmlData, rtfData] = _terminal->RetrieveSelectedTextFromBuffer(singleLine, copyHtml, copyRtf);
const auto bufferData = _terminal->RetrieveSelectedTextFromBuffer(singleLine);
// convert text: vector<string> --> string
std::wstring textData;
for (const auto& text : bufferData.text)
{
textData += text;
}
const auto bgColor = _terminal->GetAttributeColors({}).second;
// convert text to HTML format
// GH#5347 - Don't provide a title for the generated HTML, as many
// web applications will paste the title first, followed by the HTML
// content, which is unexpected.
const auto htmlData = formats == nullptr || WI_IsFlagSet(formats.Value(), CopyFormat::HTML) ?
TextBuffer::GenHTML(bufferData,
_actualFont.GetUnscaledSize().height,
_actualFont.GetFaceName(),
bgColor) :
"";
// convert to RTF format
const auto rtfData = formats == nullptr || WI_IsFlagSet(formats.Value(), CopyFormat::RTF) ?
TextBuffer::GenRTF(bufferData,
_actualFont.GetUnscaledSize().height,
_actualFont.GetFaceName(),
bgColor) :
"";
// send data up for clipboard
_CopyToClipboardHandlers(*this,
winrt::make<CopyToClipboardEventArgs>(winrt::hstring{ textData },
winrt::to_hstring(htmlData),
winrt::to_hstring(rtfData),
copyFormats));
formats));
return true;
}
@@ -1582,28 +1612,24 @@ namespace winrt::Microsoft::Terminal::Control::implementation
return _terminal->IsSelectionActive();
}
// Method Description:
// - Checks if the currently active selection spans multiple lines
// Return Value:
// - true if selection is multi-line
bool ControlCore::HasMultiLineSelection() const
{
const auto lock = _terminal->LockForReading();
assert(_terminal->IsSelectionActive()); // should only be called when selection is active
return _terminal->GetSelectionAnchor().y != _terminal->GetSelectionEnd().y;
}
bool ControlCore::CopyOnSelect() const
{
return _settings->CopyOnSelect();
}
winrt::hstring ControlCore::SelectedText(bool trimTrailingWhitespace) const
Windows::Foundation::Collections::IVector<winrt::hstring> ControlCore::SelectedText(bool trimTrailingWhitespace) const
{
// RetrieveSelectedTextFromBuffer will lock while it's reading
const auto lock = _terminal->LockForReading();
const auto internalResult{ _terminal->RetrieveSelectedTextFromBuffer(!trimTrailingWhitespace) };
return winrt::hstring{ internalResult.plainText };
const auto internalResult{ _terminal->RetrieveSelectedTextFromBuffer(trimTrailingWhitespace).text };
auto result = winrt::single_threaded_vector<winrt::hstring>();
for (const auto& row : internalResult)
{
result.Append(winrt::hstring{ row });
}
return result;
}
::Microsoft::Console::Render::IRenderData* ControlCore::GetRenderData() const

View File

@@ -5,7 +5,7 @@
// - ControlCore.h
//
// Abstract:
// - This encapsulates a `Terminal` instance, a `AtlasEngine` and `Renderer`, and
// - This encapsulates a `Terminal` instance, a `DxEngine` and `Renderer`, and
// an `ITerminalConnection`. This is intended to be everything that someone
// might need to stand up a terminal instance in a control, but without any
// regard for how the UX works.
@@ -156,8 +156,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
int BufferHeight() const;
bool HasSelection() const;
bool HasMultiLineSelection() const;
winrt::hstring SelectedText(bool trimTrailingWhitespace) const;
Windows::Foundation::Collections::IVector<winrt::hstring> SelectedText(bool trimTrailingWhitespace) const;
bool BracketedPasteEnabled() const noexcept;

View File

@@ -3,18 +3,11 @@
#include "pch.h"
#include "HwndTerminal.hpp"
#include <DefaultSettings.h>
#include <windowsx.h>
#include "HwndTerminalAutomationPeer.hpp"
#include "../../cascadia/TerminalCore/Terminal.hpp"
#include "../../renderer/atlas/AtlasEngine.h"
#include "../../renderer/base/renderer.hpp"
#include "../../renderer/uia/UiaRenderer.hpp"
#include <DefaultSettings.h>
#include "../../types/viewport.cpp"
#include "../../types/inc/GlyphWidth.hpp"
using namespace ::Microsoft::Console::VirtualTerminal;
using namespace ::Microsoft::Terminal::Core;
static LPCWSTR term_window_class = L"HwndTerminalClass";
@@ -109,23 +102,22 @@ try
}
break;
case WM_RBUTTONDOWN:
try
if (publicTerminal->_terminal && publicTerminal->_terminal->IsSelectionActive())
{
if (publicTerminal->_terminal)
try
{
const auto lock = publicTerminal->_terminal->LockForWriting();
if (publicTerminal->_terminal->IsSelectionActive())
{
const auto bufferData = publicTerminal->_terminal->RetrieveSelectedTextFromBuffer(false, true, true);
LOG_IF_FAILED(publicTerminal->_CopyTextToSystemClipboard(bufferData.plainText, bufferData.html, bufferData.rtf));
publicTerminal->_ClearSelection();
return 0;
}
const auto bufferData = publicTerminal->_terminal->RetrieveSelectedTextFromBuffer(false);
LOG_IF_FAILED(publicTerminal->_CopyTextToSystemClipboard(bufferData, true));
publicTerminal->_ClearSelection();
}
publicTerminal->_PasteTextFromClipboard();
return 0;
CATCH_LOG();
}
CATCH_LOG();
else
{
publicTerminal->_PasteTextFromClipboard();
}
return 0;
case WM_DESTROY:
// Release Terminal's hwnd so Teardown doesn't try to destroy it again
publicTerminal->_hwnd.release();
@@ -215,10 +207,10 @@ HRESULT HwndTerminal::Initialize()
RETURN_HR_IF_NULL(E_POINTER, localPointerToThread);
RETURN_IF_FAILED(localPointerToThread->Initialize(_renderer.get()));
auto engine = std::make_unique<::Microsoft::Console::Render::AtlasEngine>();
RETURN_IF_FAILED(engine->SetHwnd(_hwnd.get()));
RETURN_IF_FAILED(engine->Enable());
_renderer->AddRenderEngine(engine.get());
auto dxEngine = std::make_unique<::Microsoft::Console::Render::DxEngine>();
RETURN_IF_FAILED(dxEngine->SetHwnd(_hwnd.get()));
RETURN_IF_FAILED(dxEngine->Enable());
_renderer->AddRenderEngine(dxEngine.get());
_UpdateFont(USER_DEFAULT_SCREEN_DPI);
RECT windowRect;
@@ -229,9 +221,9 @@ HRESULT HwndTerminal::Initialize()
// Fist set up the dx engine with the window size in pixels.
// Then, using the font, get the number of characters that can fit.
const auto viewInPixels = Viewport::FromDimensions({ 0, 0 }, windowSize);
RETURN_IF_FAILED(engine->SetWindowSize({ viewInPixels.Width(), viewInPixels.Height() }));
RETURN_IF_FAILED(dxEngine->SetWindowSize({ viewInPixels.Width(), viewInPixels.Height() }));
_renderEngine = std::move(engine);
_renderEngine = std::move(dxEngine);
_terminal->Create({ 80, 25 }, 9001, *_renderer);
_terminal->SetWriteInputCallback([=](std::wstring_view input) noexcept { _WriteTextToConnection(input); });
@@ -674,14 +666,20 @@ try
return nullptr;
}
std::wstring selectedText;
TextBuffer::TextAndColor bufferData;
{
const auto lock = publicTerminal->_terminal->LockForWriting();
auto bufferData = publicTerminal->_terminal->RetrieveSelectedTextFromBuffer(false);
selectedText = std::move(bufferData.plainText);
bufferData = publicTerminal->_terminal->RetrieveSelectedTextFromBuffer(false);
publicTerminal->_ClearSelection();
}
// convert text: vector<string> --> string
std::wstring selectedText;
for (const auto& text : bufferData.text)
{
selectedText += text;
}
auto returnText = wil::make_cotaskmem_string_nothrow(selectedText.c_str());
return returnText.release();
}
@@ -754,7 +752,7 @@ try
ScreenToClient(_hwnd.get(), cursorPosition.as_win32_point());
}
const Microsoft::Console::VirtualTerminal::TerminalInput::MouseButtonState state{
const TerminalInput::MouseButtonState state{
WI_IsFlagSet(GetKeyState(VK_LBUTTON), KeyPressed),
WI_IsFlagSet(GetKeyState(VK_MBUTTON), KeyPressed),
WI_IsFlagSet(GetKeyState(VK_RBUTTON), KeyPressed)
@@ -899,7 +897,7 @@ void _stdcall TerminalSetTheme(void* terminal, TerminalTheme theme, LPCWSTR font
[[gsl::suppress(bounds .3)]] renderSettings.SetColorTableEntry(tableIndex, gsl::at(theme.ColorTable, tableIndex));
}
publicTerminal->_terminal->SetCursorStyle(static_cast<Microsoft::Console::VirtualTerminal::DispatchTypes::CursorStyle>(theme.CursorStyle));
publicTerminal->_terminal->SetCursorStyle(static_cast<DispatchTypes::CursorStyle>(theme.CursorStyle));
publicTerminal->_desiredFont = { fontFamily, 0, DEFAULT_FONT_WEIGHT, static_cast<float>(fontSize), CP_UTF8 };
publicTerminal->_UpdateFont(newDpi);
@@ -965,16 +963,22 @@ void __stdcall TerminalKillFocus(void* terminal)
// Routine Description:
// - Copies the text given onto the global system clipboard.
// Arguments:
// - text - selected text in plain-text format
// - htmlData - selected text in HTML format
// - rtfData - selected text in RTF format
HRESULT HwndTerminal::_CopyTextToSystemClipboard(const std::wstring& text, const std::string& htmlData, const std::string& rtfData) const
// - rows - Rows of text data to copy
// - fAlsoCopyFormatting - true if the color and formatting should also be copied, false otherwise
HRESULT HwndTerminal::_CopyTextToSystemClipboard(const TextBuffer::TextAndColor& rows, const bool fAlsoCopyFormatting)
try
{
RETURN_HR_IF_NULL(E_NOT_VALID_STATE, _terminal);
std::wstring finalString;
// Concatenate strings into one giant string to put onto the clipboard.
for (const auto& str : rows.text)
{
finalString += str;
}
// allocate the final clipboard data
const auto cchNeeded = text.size() + 1;
const auto cchNeeded = finalString.size() + 1;
const auto cbNeeded = sizeof(wchar_t) * cchNeeded;
wil::unique_hglobal globalHandle(GlobalAlloc(GMEM_MOVEABLE | GMEM_DDESHARE, cbNeeded));
RETURN_LAST_ERROR_IF_NULL(globalHandle.get());
@@ -984,7 +988,7 @@ try
// The pattern gets a bit strange here because there's no good wil built-in for global lock of this type.
// Try to copy then immediately unlock. Don't throw until after (so the hglobal won't be freed until we unlock).
const auto hr = StringCchCopyW(pwszClipboard, cchNeeded, text.data());
const auto hr = StringCchCopyW(pwszClipboard, cchNeeded, finalString.data());
GlobalUnlock(globalHandle.get());
RETURN_IF_FAILED(hr);
@@ -999,14 +1003,21 @@ try
RETURN_LAST_ERROR_IF(!EmptyClipboard());
RETURN_LAST_ERROR_IF_NULL(SetClipboardData(CF_UNICODETEXT, globalHandle.get()));
if (!htmlData.empty())
if (fAlsoCopyFormatting)
{
RETURN_IF_FAILED(_CopyToSystemClipboard(htmlData, L"HTML Format"));
}
const auto& fontData = _actualFont;
const int iFontHeightPoints = fontData.GetUnscaledSize().height; // this renderer uses points already
COLORREF bgColor;
{
const auto lock = _terminal->LockForReading();
bgColor = _terminal->GetAttributeColors({}).second;
}
if (!rtfData.empty())
{
RETURN_IF_FAILED(_CopyToSystemClipboard(rtfData, L"Rich Text Format"));
auto HTMLToPlaceOnClip = TextBuffer::GenHTML(rows, iFontHeightPoints, fontData.GetFaceName(), bgColor);
_CopyToSystemClipboard(HTMLToPlaceOnClip, L"HTML Format");
auto RTFToPlaceOnClip = TextBuffer::GenRTF(rows, iFontHeightPoints, fontData.GetFaceName(), bgColor);
_CopyToSystemClipboard(RTFToPlaceOnClip, L"Rich Text Format");
}
}
@@ -1024,7 +1035,7 @@ CATCH_RETURN()
// Arguments:
// - stringToCopy - The string to copy
// - lpszFormat - the name of the format
HRESULT HwndTerminal::_CopyToSystemClipboard(const std::string& stringToCopy, LPCWSTR lpszFormat) const
HRESULT HwndTerminal::_CopyToSystemClipboard(std::string stringToCopy, LPCWSTR lpszFormat)
{
const auto cbData = stringToCopy.size() + 1; // +1 for '\0'
if (cbData)

View File

@@ -3,31 +3,14 @@
#pragma once
#include "../../buffer/out/textBuffer.hpp"
#include "../../renderer/inc/FontInfoDesired.hpp"
#include "../../renderer/base/Renderer.hpp"
#include "../../renderer/dx/DxRenderer.hpp"
#include "../../renderer/uia/UiaRenderer.hpp"
#include "../../cascadia/TerminalCore/Terminal.hpp"
#include "../../types/IControlAccessibilityInfo.h"
#include "HwndTerminalAutomationPeer.hpp"
namespace Microsoft::Console::Render::Atlas
{
class AtlasEngine;
}
namespace Microsoft::Console::Render
{
using AtlasEngine = Atlas::AtlasEngine;
class IRenderData;
class Renderer;
class UiaEngine;
}
namespace Microsoft::Terminal::Core
{
class Terminal;
}
class FontInfo;
class FontInfoDesired;
class HwndTerminalAutomationPeer;
using namespace Microsoft::Console::VirtualTerminal;
// Keep in sync with TerminalTheme.cs
typedef struct _TerminalTheme
@@ -96,7 +79,7 @@ private:
std::unique_ptr<::Microsoft::Terminal::Core::Terminal> _terminal;
std::unique_ptr<::Microsoft::Console::Render::Renderer> _renderer;
std::unique_ptr<::Microsoft::Console::Render::AtlasEngine> _renderEngine;
std::unique_ptr<::Microsoft::Console::Render::DxEngine> _renderEngine;
std::unique_ptr<::Microsoft::Console::Render::UiaEngine> _uiaEngine;
bool _focused{ false };
@@ -126,8 +109,8 @@ private:
void _UpdateFont(int newDpi);
void _WriteTextToConnection(const std::wstring_view text) noexcept;
HRESULT _CopyTextToSystemClipboard(const std::wstring& text, const std::string& htmlData, const std::string& rtfData) const;
HRESULT _CopyToSystemClipboard(const std::string& stringToCopy, LPCWSTR lpszFormat) const;
HRESULT _CopyTextToSystemClipboard(const TextBuffer::TextAndColor& rows, const bool fAlsoCopyFormatting);
HRESULT _CopyToSystemClipboard(std::string stringToCopy, LPCWSTR lpszFormat);
void _PasteTextFromClipboard() noexcept;
const unsigned int _NumberOfClicks(til::point clickPos, std::chrono::steady_clock::time_point clickTime) noexcept;

View File

@@ -3,7 +3,6 @@
import "IKeyBindings.idl";
import "IControlAppearance.idl";
import "EventArgs.idl";
namespace Microsoft.Terminal.Control
{
@@ -35,6 +34,8 @@ namespace Microsoft.Terminal.Control
Boolean EnableUnfocusedAcrylic;
ScrollbarState ScrollState { get; };
Boolean UseAtlasEngine { get; };
String FontFace { get; };
Single FontSize { get; };
Windows.UI.Text.FontWeight FontWeight { get; };
@@ -47,7 +48,6 @@ namespace Microsoft.Terminal.Control
Microsoft.Terminal.Control.IKeyBindings KeyBindings { get; };
Boolean CopyOnSelect { get; };
Microsoft.Terminal.Control.CopyFormat CopyFormatting { get; };
Boolean FocusFollowMouse { get; };
String Commandline { get; };

View File

@@ -46,8 +46,7 @@ namespace Microsoft.Terminal.Control
Int32 BufferHeight { get; };
Boolean HasSelection { get; };
Boolean HasMultiLineSelection { get; };
String SelectedText(Boolean trimTrailingWhitespace);
IVector<String> SelectedText(Boolean trimTrailingWhitespace);
Boolean BracketedPasteEnabled { get; };

View File

@@ -57,7 +57,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation
_isInternalScrollBarUpdate{ false },
_autoScrollVelocity{ 0 },
_autoScrollingPointerPoint{ std::nullopt },
_autoScrollTimer{},
_lastAutoScrollUpdateTime{ std::nullopt },
_cursorTimer{},
_blinkTimer{},
_searchBox{ nullptr }
{
InitializeComponent();
@@ -416,9 +419,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// Currently we populate the search box only if a single line is selected.
// Empirically, multi-line selection works as well on sample scenarios,
// but since code paths differ, extra work is required to ensure correctness.
if (!_core.HasMultiLineSelection())
auto bufferText = _core.SelectedText(true);
if (bufferText.Size() == 1)
{
const auto selectedLine{ _core.SelectedText(true) };
const auto selectedLine{ bufferText.GetAt(0) };
_searchBox->PopulateTextbox(selectedLine);
}
}
@@ -1083,8 +1087,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation
if (blinkTime != INFINITE)
{
// Create a timer
_cursorTimer.Interval(std::chrono::milliseconds(blinkTime));
_cursorTimer.Tick({ get_weak(), &TermControl::_CursorTimerTick });
DispatcherTimer cursorTimer;
cursorTimer.Interval(std::chrono::milliseconds(blinkTime));
cursorTimer.Tick({ get_weak(), &TermControl::_CursorTimerTick });
_cursorTimer.emplace(std::move(cursorTimer));
// As of GH#6586, don't start the cursor timer immediately, and
// don't show the cursor initially. We'll show the cursor and start
// the timer when the control is first focused.
@@ -1099,12 +1105,13 @@ namespace winrt::Microsoft::Terminal::Control::implementation
_core.CursorOn(_focused || _displayCursorWhileBlurred());
if (_displayCursorWhileBlurred())
{
_cursorTimer.Start();
_cursorTimer->Start();
}
}
else
{
_cursorTimer.Destroy();
// The user has disabled cursor blinking
_cursorTimer = std::nullopt;
}
// Set up blinking attributes
@@ -1113,14 +1120,16 @@ namespace winrt::Microsoft::Terminal::Control::implementation
if (animationsEnabled && blinkTime != INFINITE)
{
// Create a timer
_blinkTimer.Interval(std::chrono::milliseconds(blinkTime));
_blinkTimer.Tick({ get_weak(), &TermControl::_BlinkTimerTick });
_blinkTimer.Start();
DispatcherTimer blinkTimer;
blinkTimer.Interval(std::chrono::milliseconds(blinkTime));
blinkTimer.Tick({ get_weak(), &TermControl::_BlinkTimerTick });
blinkTimer.Start();
_blinkTimer.emplace(std::move(blinkTimer));
}
else
{
// The user has disabled blinking
_blinkTimer.Destroy();
_blinkTimer = std::nullopt;
}
// Now that the renderer is set up, update the appearance for initialization
@@ -1336,7 +1345,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// 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() && !modifiers.IsCtrlPressed() &&
if (modifiers.IsAltPressed() &&
(vkey >= VK_NUMPAD0 && vkey <= VK_NUMPAD9))
{
e.Handled(true);
@@ -1489,7 +1498,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// Manually show the cursor when a key is pressed. Restarting
// the timer prevents flickering.
_core.CursorOn(_core.SelectionMode() != SelectionInteractionMode::Mark);
_cursorTimer.Start();
_cursorTimer->Start();
}
return handled;
@@ -1964,12 +1973,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation
{
// When the terminal focuses, show the cursor immediately
_core.CursorOn(_core.SelectionMode() != SelectionInteractionMode::Mark);
_cursorTimer.Start();
_cursorTimer->Start();
}
if (_blinkTimer)
{
_blinkTimer.Start();
_blinkTimer->Start();
}
// Only update the appearance here if an unfocused config exists - if an
@@ -2012,13 +2021,13 @@ namespace winrt::Microsoft::Terminal::Control::implementation
if (_cursorTimer && !_displayCursorWhileBlurred())
{
_cursorTimer.Stop();
_cursorTimer->Stop();
_core.CursorOn(false);
}
if (_blinkTimer)
{
_blinkTimer.Stop();
_blinkTimer->Stop();
}
// Check if there is an unfocused config we should set the appearance to
@@ -2269,16 +2278,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// Disconnect the TSF input control so it doesn't receive EditContext events.
TSFInputControl().Close();
// At the time of writing, closing the last tab of a window inexplicably
// does not lead to the destruction of the remaining TermControl instance(s).
// On Win10 we don't destroy window threads due to bugs in DesktopWindowXamlSource.
// In turn, we leak TermControl instances. This results in constant HWND messages
// while the thread is supposed to be idle. Stop these timers avoids this.
_autoScrollTimer.Stop();
_bellLightTimer.Stop();
_cursorTimer.Stop();
_blinkTimer.Stop();
if (!_detached)
{
@@ -2404,14 +2404,25 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// then use it to measure how much space the requested rows and columns
// will take up.
// TODO: MSFT:21254947 - use a static function to do this instead of
// instantiating a AtlasEngine.
// instantiating a DxEngine/AtlasEngine.
// GH#10211 - UNDER NO CIRCUMSTANCE should this fail. If it does, the
// whole app will crash instantaneously on launch, which is no good.
const auto engine = std::make_unique<::Microsoft::Console::Render::AtlasEngine>();
LOG_IF_FAILED(engine->UpdateDpi(dpi));
LOG_IF_FAILED(engine->UpdateFont(desiredFont, actualFont));
float scale;
if (settings.UseAtlasEngine())
{
auto engine = std::make_unique<::Microsoft::Console::Render::AtlasEngine>();
LOG_IF_FAILED(engine->UpdateDpi(dpi));
LOG_IF_FAILED(engine->UpdateFont(desiredFont, actualFont));
scale = engine->GetScaling();
}
else
{
auto engine = std::make_unique<::Microsoft::Console::Render::DxEngine>();
LOG_IF_FAILED(engine->UpdateDpi(dpi));
LOG_IF_FAILED(engine->UpdateFont(desiredFont, actualFont));
scale = engine->GetScaling();
}
const auto scale = engine->GetScaling();
const auto actualFontSize = actualFont.GetSize();
// UWP XAML scrollbars aren't guaranteed to be the same size as the
@@ -3118,13 +3129,20 @@ namespace winrt::Microsoft::Terminal::Control::implementation
_bellDarkAnimation.Duration(winrt::Windows::Foundation::TimeSpan(std::chrono::milliseconds(TerminalWarningBellInterval)));
}
// Similar to the animation, only initialize the timer here
if (!_bellLightTimer)
{
_bellLightTimer = {};
_bellLightTimer.Interval(std::chrono::milliseconds(TerminalWarningBellInterval));
_bellLightTimer.Tick({ get_weak(), &TermControl::_BellLightOff });
}
Windows::Foundation::Numerics::float2 zeroSize{ 0, 0 };
// If the grid has 0 size or if the bell timer is
// already active, do nothing
if (RootGrid().ActualSize() != zeroSize && !_bellLightTimer.IsEnabled())
{
_bellLightTimer.Interval(std::chrono::milliseconds(TerminalWarningBellInterval));
_bellLightTimer.Tick({ get_weak(), &TermControl::_BellLightOff });
// Start the timer, when the timer ticks we switch off the light
_bellLightTimer.Start();
// Switch on the light and animate the intensity to fade out
@@ -3144,12 +3162,15 @@ namespace winrt::Microsoft::Terminal::Control::implementation
void TermControl::_BellLightOff(const Windows::Foundation::IInspectable& /* sender */,
const Windows::Foundation::IInspectable& /* e */)
{
// Stop the timer and switch off the light
_bellLightTimer.Stop();
if (!_IsClosing())
if (_bellLightTimer)
{
VisualBellLight::SetIsTarget(RootGrid(), false);
// Stop the timer and switch off the light
_bellLightTimer.Stop();
if (!_IsClosing())
{
VisualBellLight::SetIsTarget(RootGrid(), false);
}
}
}
@@ -3475,11 +3496,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
{
return _core.HasSelection();
}
bool TermControl::HasMultiLineSelection() const
{
return _core.HasMultiLineSelection();
}
winrt::hstring TermControl::SelectedText(bool trimTrailingWhitespace) const
Windows::Foundation::Collections::IVector<winrt::hstring> TermControl::SelectedText(bool trimTrailingWhitespace) const
{
return _core.SelectedText(trimTrailingWhitespace);
}
@@ -3712,9 +3729,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation
{
// If we should be ALWAYS displaying the cursor, turn it on and start blinking.
_core.CursorOn(true);
if (_cursorTimer)
if (_cursorTimer.has_value())
{
_cursorTimer.Start();
_cursorTimer->Start();
}
}
else
@@ -3723,9 +3740,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// blinking. (if we're focused, then we're already doing the right
// thing)
const auto focused = FocusState() != FocusState::Unfocused;
if (!focused && _cursorTimer)
if (!focused && _cursorTimer.has_value())
{
_cursorTimer.Stop();
_cursorTimer->Stop();
}
_core.CursorOn(focused);
}

View File

@@ -7,6 +7,7 @@
#include "XamlLights.h"
#include "EventArgs.h"
#include "../../renderer/base/Renderer.hpp"
#include "../../renderer/dx/DxRenderer.hpp"
#include "../../renderer/uia/UiaRenderer.hpp"
#include "../../cascadia/TerminalCore/Terminal.hpp"
#include "../buffer/out/search.h"
@@ -71,8 +72,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
int BufferHeight() const;
bool HasSelection() const;
bool HasMultiLineSelection() const;
winrt::hstring SelectedText(bool trimTrailingWhitespace) const;
Windows::Foundation::Collections::IVector<winrt::hstring> SelectedText(bool trimTrailingWhitespace) const;
bool BracketedPasteEnabled() const noexcept;
@@ -236,16 +236,16 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// viewport. View is then scrolled to 'follow' the cursor.
double _autoScrollVelocity;
std::optional<Windows::UI::Input::PointerPoint> _autoScrollingPointerPoint;
SafeDispatcherTimer _autoScrollTimer;
Windows::UI::Xaml::DispatcherTimer _autoScrollTimer;
std::optional<std::chrono::high_resolution_clock::time_point> _lastAutoScrollUpdateTime;
bool _pointerPressedInBounds{ false };
winrt::Windows::UI::Composition::ScalarKeyFrameAnimation _bellLightAnimation{ nullptr };
winrt::Windows::UI::Composition::ScalarKeyFrameAnimation _bellDarkAnimation{ nullptr };
SafeDispatcherTimer _bellLightTimer;
Windows::UI::Xaml::DispatcherTimer _bellLightTimer{ nullptr };
SafeDispatcherTimer _cursorTimer;
SafeDispatcherTimer _blinkTimer;
std::optional<Windows::UI::Xaml::DispatcherTimer> _cursorTimer;
std::optional<Windows::UI::Xaml::DispatcherTimer> _blinkTimer;
winrt::Windows::UI::Xaml::Controls::SwapChainPanel::LayoutUpdated_revoker _layoutUpdatedRevoker;
bool _showMarksInScrollbar{ false };

View File

@@ -31,7 +31,7 @@
<Import Project="..\..\..\common.openconsole.props" Condition="'$(OpenConsoleDir)'==''" />
<Import Project="$(OpenConsoleDir)src\common.nugetversions.props" />
<Import Project="$(OpenConsoleDir)src\cppwinrt.build.pre.props" />
<!-- ========================= Headers ======================== -->
<ItemGroup>
<ClInclude Include="pch.h" />
@@ -166,6 +166,7 @@
<ProjectReference Include="..\..\buffer\out\lib\bufferout.vcxproj" />
<ProjectReference Include="$(OpenConsoleDir)src\renderer\base\lib\base.vcxproj" />
<ProjectReference Include="..\..\renderer\atlas\atlas.vcxproj" />
<ProjectReference Include="..\..\renderer\dx\lib\dx.vcxproj" />
<ProjectReference Include="..\..\renderer\uia\lib\uia.vcxproj" />
<ProjectReference Include="..\..\terminal\parser\lib\parser.vcxproj" />
<ProjectReference Include="$(OpenConsoleDir)src\terminal\input\lib\terminalinput.vcxproj" />

View File

@@ -92,8 +92,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation
winrt::Windows::Foundation::IInspectable XamlUiaTextRange::GetAttributeValue(int32_t textAttributeId) const
{
// Call the function off of the underlying UiaTextRange.
wil::unique_variant result;
THROW_IF_FAILED(_uiaProvider->GetAttributeValue(textAttributeId, result.addressof()));
VARIANT result;
THROW_IF_FAILED(_uiaProvider->GetAttributeValue(textAttributeId, &result));
// Convert the resulting VARIANT into a format that is consumable by XAML.
switch (result.vt)
@@ -189,9 +189,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation
winrt::hstring XamlUiaTextRange::GetText(int32_t maxLength) const
{
wil::unique_bstr returnVal;
THROW_IF_FAILED(_uiaProvider->GetText(maxLength, returnVal.put()));
return winrt::hstring{ returnVal.get(), SysStringLen(returnVal.get()) };
BSTR returnVal;
THROW_IF_FAILED(_uiaProvider->GetText(maxLength, &returnVal));
return winrt::to_hstring(returnVal);
}
int32_t XamlUiaTextRange::Move(XamlAutomation::TextUnit unit,

View File

@@ -73,8 +73,7 @@ TRACELOGGING_DECLARE_PROVIDER(g_hTerminalControlProvider);
#include <til/mutex.h>
#include <til/winrt.h>
#include <SafeDispatcherTimer.h>
#include <ThrottledFunc.h>
#include "ThrottledFunc.h"
#include <cppwinrt_utils.h>
#include <wil/cppwinrt_helpers.h> // must go after the CoreDispatcher type is defined

View File

@@ -293,13 +293,6 @@ public:
End = 0x2
};
struct TextCopyData
{
std::wstring plainText;
std::string html;
std::string rtf;
};
void MultiClickSelection(const til::point viewportPos, SelectionExpansion expansionMode);
void SetSelectionAnchor(const til::point position);
void SetSelectionEnd(const til::point position, std::optional<SelectionExpansion> newExpansionMode = std::nullopt);
@@ -318,7 +311,7 @@ public:
til::point SelectionEndForRendering() const;
const SelectionEndpoint SelectionEndpointTarget() const noexcept;
TextCopyData RetrieveSelectedTextFromBuffer(const bool singleLine, const bool html = false, const bool rtf = false) const;
const TextBuffer::TextAndColor RetrieveSelectedTextFromBuffer(bool trimTrailingWhitespace);
#pragma endregion
#ifndef NDEBUG

View File

@@ -83,7 +83,7 @@ std::vector<til::inclusive_rect> Terminal::_GetSearchSelectionRects(Microsoft::C
for (auto selection = lowerIt; selection != upperIt; ++selection)
{
const auto start = til::point{ selection->left, selection->top };
const auto end = til::point{ selection->right, selection->bottom };
const auto end = til::point{ selection->right, selection->top };
const auto adj = _activeBuffer().GetTextRects(start, end, _blockSelection, false);
for (auto a : adj)
{
@@ -867,53 +867,27 @@ void Terminal::ClearSelection()
}
// Method Description:
// - Get text from highlighted portion of text buffer
// - Optionally, get the highlighted text in HTML and RTF formats
// - get wstring text from highlighted portion of text buffer
// Arguments:
// - singleLine: collapse all of the text to one line. (Turns off trailing whitespace trimming)
// - html: also get text in HTML format
// - rtf: also get text in RTF format
// - singleLine: collapse all of the text to one line
// Return Value:
// - Plain and formatted selected text from buffer. Empty string represents no data for that format.
// - If extended to multiple lines, each line is separated by \r\n
Terminal::TextCopyData Terminal::RetrieveSelectedTextFromBuffer(const bool singleLine, const bool html, const bool rtf) const
// - wstring text from buffer. If extended to multiple lines, each line is separated by \r\n
const TextBuffer::TextAndColor Terminal::RetrieveSelectedTextFromBuffer(bool singleLine)
{
TextCopyData data;
if (!IsSelectionActive())
{
return data;
}
const auto selectionRects = _GetSelectionRects();
const auto GetAttributeColors = [&](const auto& attr) {
const auto [fg, bg] = _renderSettings.GetAttributeColors(attr);
const auto ul = _renderSettings.GetAttributeUnderlineColor(attr);
return std::tuple{ fg, bg, ul };
return _renderSettings.GetAttributeColors(attr);
};
const auto& textBuffer = _activeBuffer();
const auto req = TextBuffer::CopyRequest::FromConfig(textBuffer, _selection->start, _selection->end, singleLine, _blockSelection, _trimBlockSelection);
data.plainText = textBuffer.GetPlainText(req);
if (html || rtf)
{
const auto bgColor = _renderSettings.GetAttributeColors({}).second;
const auto isIntenseBold = _renderSettings.GetRenderMode(::Microsoft::Console::Render::RenderSettings::Mode::IntenseIsBold);
const auto fontSizePt = _fontInfo.GetUnscaledSize().height; // already in points
const auto& fontName = _fontInfo.GetFaceName();
if (html)
{
data.html = textBuffer.GenHTML(req, fontSizePt, fontName, bgColor, isIntenseBold, GetAttributeColors);
}
if (rtf)
{
data.rtf = textBuffer.GenRTF(req, fontSizePt, fontName, bgColor, isIntenseBold, GetAttributeColors);
}
}
return data;
// GH#6740: Block selection should preserve the visual structure:
// - CRLFs need to be added - so the lines structure is preserved
// - We should apply formatting above to wrapped rows as well (newline should be added).
// GH#9706: Trimming of trailing white-spaces in block selection is configurable.
const auto includeCRLF = !singleLine || _blockSelection;
const auto trimTrailingWhitespace = !singleLine && (!_blockSelection || _trimBlockSelection);
const auto formatWrappedRows = _blockSelection;
return _activeBuffer().GetText(includeCRLF, trimTrailingWhitespace, selectionRects, GetAttributeColors, formatWrappedRows);
}
// Method Description:

View File

@@ -43,6 +43,9 @@
<ProjectReference Include="$(OpenConsoleDir)src\renderer\atlas\atlas.vcxproj">
<Project>{8222900C-8B6C-452A-91AC-BE95DB04B95F}</Project>
</ProjectReference>
<ProjectReference Include="$(OpenConsoleDir)src\renderer\dx\lib\dx.vcxproj">
<Project>{48d21369-3d7b-4431-9967-24e81292cf62}</Project>
</ProjectReference>
<ProjectReference Include="$(OpenConsoleDir)src\audio\midi\lib\midi.vcxproj">
<Project>{3c67784e-1453-49c2-9660-483e2cc7f7ad}</Project>
</ProjectReference>

View File

@@ -4,14 +4,108 @@
#include "pch.h"
#include "Actions.h"
#include "Actions.g.cpp"
#include "KeyBindingViewModel.g.cpp"
#include "ActionsPageNavigationState.g.cpp"
#include "LibraryResources.h"
#include "../TerminalSettingsModel/AllShortcutActions.h"
using namespace winrt::Windows::Foundation;
using namespace winrt::Windows::Foundation::Collections;
using namespace winrt::Windows::System;
using namespace winrt::Windows::UI::Core;
using namespace winrt::Windows::UI::Xaml;
using namespace winrt::Windows::UI::Xaml::Controls;
using namespace winrt::Windows::UI::Xaml::Data;
using namespace winrt::Windows::UI::Xaml::Navigation;
using namespace winrt::Microsoft::Terminal::Settings::Model;
namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
KeyBindingViewModel::KeyBindingViewModel(const Windows::Foundation::Collections::IObservableVector<hstring>& availableActions) :
KeyBindingViewModel(nullptr, availableActions.First().Current(), availableActions) {}
KeyBindingViewModel::KeyBindingViewModel(const Control::KeyChord& keys, const hstring& actionName, const IObservableVector<hstring>& availableActions) :
_CurrentKeys{ keys },
_KeyChordText{ KeyChordSerialization::ToString(keys) },
_CurrentAction{ actionName },
_ProposedAction{ box_value(actionName) },
_AvailableActions{ availableActions }
{
// Add a property changed handler to our own property changed event.
// This propagates changes from the settings model to anybody listening to our
// unique view model members.
PropertyChanged([this](auto&&, const PropertyChangedEventArgs& args) {
const auto viewModelProperty{ args.PropertyName() };
if (viewModelProperty == L"CurrentKeys")
{
_KeyChordText = KeyChordSerialization::ToString(_CurrentKeys);
_NotifyChanges(L"KeyChordText");
}
else if (viewModelProperty == L"IsContainerFocused" ||
viewModelProperty == L"IsEditButtonFocused" ||
viewModelProperty == L"IsHovered" ||
viewModelProperty == L"IsAutomationPeerAttached" ||
viewModelProperty == L"IsInEditMode")
{
_NotifyChanges(L"ShowEditButton");
}
else if (viewModelProperty == L"CurrentAction")
{
_NotifyChanges(L"Name");
}
});
}
hstring KeyBindingViewModel::EditButtonName() const noexcept { return RS_(L"Actions_EditButton/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip"); }
hstring KeyBindingViewModel::CancelButtonName() const noexcept { return RS_(L"Actions_CancelButton/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip"); }
hstring KeyBindingViewModel::AcceptButtonName() const noexcept { return RS_(L"Actions_AcceptButton/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip"); }
hstring KeyBindingViewModel::DeleteButtonName() const noexcept { return RS_(L"Actions_DeleteButton/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip"); }
bool KeyBindingViewModel::ShowEditButton() const noexcept
{
return (IsContainerFocused() || IsEditButtonFocused() || IsHovered() || IsAutomationPeerAttached()) && !IsInEditMode();
}
void KeyBindingViewModel::ToggleEditMode()
{
// toggle edit mode
IsInEditMode(!_IsInEditMode);
if (_IsInEditMode)
{
// if we're in edit mode,
// - pre-populate the text box with the current keys
// - reset the combo box with the current action
ProposedKeys(_CurrentKeys);
ProposedAction(box_value(_CurrentAction));
}
}
void KeyBindingViewModel::AttemptAcceptChanges()
{
AttemptAcceptChanges(_ProposedKeys);
}
void KeyBindingViewModel::AttemptAcceptChanges(const Control::KeyChord newKeys)
{
const auto args{ make_self<ModifyKeyBindingEventArgs>(_CurrentKeys, // OldKeys
newKeys, // NewKeys
_IsNewlyAdded ? hstring{} : _CurrentAction, // OldAction
unbox_value<hstring>(_ProposedAction)) }; // NewAction
_ModifyKeyBindingRequestedHandlers(*this, *args);
}
void KeyBindingViewModel::CancelChanges()
{
if (_IsNewlyAdded)
{
_DeleteNewlyAddedKeyBindingHandlers(*this, nullptr);
}
else
{
ToggleEditMode();
}
}
Actions::Actions()
{
InitializeComponent();
@@ -21,55 +115,277 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
Automation::Peers::AutomationPeer Actions::OnCreateAutomationPeer()
{
_ViewModel.OnAutomationPeerAttached();
_AutomationPeerAttached = true;
for (const auto& kbdVM : _KeyBindingList)
{
// To create a more accessible experience, we want the "edit" buttons to _always_
// appear when a screen reader is attached. This ensures that the edit buttons are
// accessible via the UIA tree.
get_self<KeyBindingViewModel>(kbdVM)->IsAutomationPeerAttached(_AutomationPeerAttached);
}
return nullptr;
}
void Actions::OnNavigatedTo(const NavigationEventArgs& e)
{
_ViewModel = e.Parameter().as<Editor::ActionsViewModel>();
_State = e.Parameter().as<Editor::ActionsPageNavigationState>();
// Subscribe to the view model's FocusContainer event.
// Use the KeyBindingViewModel or index provided in the event to focus the corresponding container
_ViewModel.FocusContainer([this](const auto& /*sender*/, const auto& args) {
if (auto kbdVM{ args.try_as<KeyBindingViewModel>() })
{
if (const auto& container = KeyBindingsListView().ContainerFromItem(*kbdVM))
{
container.as<Controls::ListViewItem>().Focus(FocusState::Programmatic);
}
}
else if (const auto& index = args.try_as<uint32_t>())
{
if (const auto& container = KeyBindingsListView().ContainerFromIndex(*index))
{
container.as<Controls::ListViewItem>().Focus(FocusState::Programmatic);
}
}
});
// Populate AvailableActionAndArgs
_AvailableActionMap = single_threaded_map<hstring, Model::ActionAndArgs>();
std::vector<hstring> availableActionAndArgs;
for (const auto& [name, actionAndArgs] : _State.Settings().ActionMap().AvailableActions())
{
availableActionAndArgs.push_back(name);
_AvailableActionMap.Insert(name, actionAndArgs);
}
std::sort(begin(availableActionAndArgs), end(availableActionAndArgs));
_AvailableActionAndArgs = single_threaded_observable_vector(std::move(availableActionAndArgs));
// Subscribe to the view model's UpdateBackground event.
// The view model does not have access to the page resources, so it asks us
// to update the key binding's container background
_ViewModel.UpdateBackground([this](const auto& /*sender*/, const auto& args) {
if (auto kbdVM{ args.try_as<KeyBindingViewModel>() })
{
if (kbdVM->IsInEditMode())
{
const auto& containerBackground{ Resources().Lookup(box_value(L"ActionContainerBackgroundEditing")).as<Windows::UI::Xaml::Media::Brush>() };
kbdVM->ContainerBackground(containerBackground);
}
else
{
const auto& containerBackground{ Resources().Lookup(box_value(L"ActionContainerBackground")).as<Windows::UI::Xaml::Media::Brush>() };
kbdVM->ContainerBackground(containerBackground);
}
}
});
// Convert the key bindings from our settings into a view model representation
const auto& keyBindingMap{ _State.Settings().ActionMap().KeyBindings() };
std::vector<Editor::KeyBindingViewModel> keyBindingList;
keyBindingList.reserve(keyBindingMap.Size());
for (const auto& [keys, cmd] : keyBindingMap)
{
// convert the cmd into a KeyBindingViewModel
auto container{ make_self<KeyBindingViewModel>(keys, cmd.Name(), _AvailableActionAndArgs) };
_RegisterEvents(container);
keyBindingList.push_back(*container);
}
std::sort(begin(keyBindingList), end(keyBindingList), KeyBindingViewModelComparator{});
_KeyBindingList = single_threaded_observable_vector(std::move(keyBindingList));
}
void Actions::AddNew_Click(const IInspectable& /*sender*/, const RoutedEventArgs& /*eventArgs*/)
{
_ViewModel.AddNewKeybinding();
// Create the new key binding and register all of the event handlers.
auto kbdVM{ make_self<KeyBindingViewModel>(_AvailableActionAndArgs) };
_RegisterEvents(kbdVM);
kbdVM->DeleteNewlyAddedKeyBinding({ this, &Actions::_ViewModelDeleteNewlyAddedKeyBindingHandler });
// Manually add the editing background. This needs to be done in Actions not the view model.
// We also have to do this manually because it hasn't been added to the list yet.
kbdVM->IsInEditMode(true);
const auto& containerBackground{ Resources().Lookup(box_value(L"ActionContainerBackgroundEditing")).as<Windows::UI::Xaml::Media::Brush>() };
kbdVM->ContainerBackground(containerBackground);
// IMPORTANT: do this _after_ setting IsInEditMode. Otherwise, it'll get deleted immediately
// by the PropertyChangedHandler below (where we delete any IsNewlyAdded items)
kbdVM->IsNewlyAdded(true);
_KeyBindingList.InsertAt(0, *kbdVM);
}
void Actions::_ViewModelPropertyChangedHandler(const IInspectable& sender, const Windows::UI::Xaml::Data::PropertyChangedEventArgs& args)
{
const auto senderVM{ sender.as<Editor::KeyBindingViewModel>() };
const auto propertyName{ args.PropertyName() };
if (propertyName == L"IsInEditMode")
{
if (senderVM.IsInEditMode())
{
// Ensure that...
// 1. we move focus to the edit mode controls
// 2. any actions that were newly added are removed
// 3. this is the only entry that is in edit mode
for (int32_t i = _KeyBindingList.Size() - 1; i >= 0; --i)
{
const auto& kbdVM{ _KeyBindingList.GetAt(i) };
if (senderVM == kbdVM)
{
// This is the view model entry that went into edit mode.
// Move focus to the edit mode controls by
// extracting the list view item container.
const auto& container{ KeyBindingsListView().ContainerFromIndex(i).try_as<ListViewItem>() };
container.Focus(FocusState::Programmatic);
}
else if (kbdVM.IsNewlyAdded())
{
// Remove any actions that were newly added
_KeyBindingList.RemoveAt(i);
}
else
{
// Exit edit mode for all other containers
get_self<KeyBindingViewModel>(kbdVM)->DisableEditMode();
}
}
const auto& containerBackground{ Resources().Lookup(box_value(L"ActionContainerBackgroundEditing")).as<Windows::UI::Xaml::Media::Brush>() };
get_self<KeyBindingViewModel>(senderVM)->ContainerBackground(containerBackground);
}
else
{
// Focus on the list view item
KeyBindingsListView().ContainerFromItem(senderVM).as<Controls::Control>().Focus(FocusState::Programmatic);
const auto& containerBackground{ Resources().Lookup(box_value(L"ActionContainerBackground")).as<Windows::UI::Xaml::Media::Brush>() };
get_self<KeyBindingViewModel>(senderVM)->ContainerBackground(containerBackground);
}
}
}
void Actions::_ViewModelDeleteKeyBindingHandler(const Editor::KeyBindingViewModel& senderVM, const Control::KeyChord& keys)
{
// Update the settings model
_State.Settings().ActionMap().DeleteKeyBinding(keys);
// Find the current container in our list and remove it.
// This is much faster than rebuilding the entire ActionMap.
uint32_t index;
if (_KeyBindingList.IndexOf(senderVM, index))
{
_KeyBindingList.RemoveAt(index);
// Focus the new item at this index
if (_KeyBindingList.Size() != 0)
{
const auto newFocusedIndex{ std::clamp(index, 0u, _KeyBindingList.Size() - 1) };
KeyBindingsListView().ContainerFromIndex(newFocusedIndex).as<Controls::Control>().Focus(FocusState::Programmatic);
}
}
}
void Actions::_ViewModelModifyKeyBindingHandler(const Editor::KeyBindingViewModel& senderVM, const Editor::ModifyKeyBindingEventArgs& args)
{
const auto isNewAction{ !args.OldKeys() && args.OldActionName().empty() };
auto applyChangesToSettingsModel = [=]() {
// If the key chord was changed,
// update the settings model and view model appropriately
// NOTE: we still need to update the view model if we're working with a newly added action
if (isNewAction || args.OldKeys().Modifiers() != args.NewKeys().Modifiers() || args.OldKeys().Vkey() != args.NewKeys().Vkey())
{
if (!isNewAction)
{
// update settings model
_State.Settings().ActionMap().RebindKeys(args.OldKeys(), args.NewKeys());
}
// update view model
auto senderVMImpl{ get_self<KeyBindingViewModel>(senderVM) };
senderVMImpl->CurrentKeys(args.NewKeys());
}
// If the action was changed,
// update the settings model and view model appropriately
// NOTE: no need to check for "isNewAction" here. <empty_string> != <action name> already.
if (args.OldActionName() != args.NewActionName())
{
// convert the action's name into a view model.
const auto& newAction{ _AvailableActionMap.Lookup(args.NewActionName()) };
// update settings model
_State.Settings().ActionMap().RegisterKeyBinding(args.NewKeys(), newAction);
// update view model
auto senderVMImpl{ get_self<KeyBindingViewModel>(senderVM) };
senderVMImpl->CurrentAction(args.NewActionName());
senderVMImpl->IsNewlyAdded(false);
}
};
// Check for this special case:
// we're changing the key chord,
// but the new key chord is already in use
if (isNewAction || args.OldKeys().Modifiers() != args.NewKeys().Modifiers() || args.OldKeys().Vkey() != args.NewKeys().Vkey())
{
const auto& conflictingCmd{ _State.Settings().ActionMap().GetActionByKeyChord(args.NewKeys()) };
if (conflictingCmd)
{
// We're about to overwrite another key chord.
// Display a confirmation dialog.
TextBlock errorMessageTB{};
errorMessageTB.Text(RS_(L"Actions_RenameConflictConfirmationMessage"));
const auto conflictingCmdName{ conflictingCmd.Name() };
TextBlock conflictingCommandNameTB{};
conflictingCommandNameTB.Text(fmt::format(L"\"{}\"", conflictingCmdName.empty() ? RS_(L"Actions_UnnamedCommandName") : conflictingCmdName));
conflictingCommandNameTB.FontStyle(Windows::UI::Text::FontStyle::Italic);
TextBlock confirmationQuestionTB{};
confirmationQuestionTB.Text(RS_(L"Actions_RenameConflictConfirmationQuestion"));
Button acceptBTN{};
acceptBTN.Content(box_value(RS_(L"Actions_RenameConflictConfirmationAcceptButton")));
acceptBTN.Click([=](auto&, auto&) {
// remove conflicting key binding from list view
const auto containerIndex{ _GetContainerIndexByKeyChord(args.NewKeys()) };
_KeyBindingList.RemoveAt(*containerIndex);
// remove flyout
senderVM.AcceptChangesFlyout().Hide();
senderVM.AcceptChangesFlyout(nullptr);
// update settings model and view model
applyChangesToSettingsModel();
senderVM.ToggleEditMode();
});
StackPanel flyoutStack{};
flyoutStack.Children().Append(errorMessageTB);
flyoutStack.Children().Append(conflictingCommandNameTB);
flyoutStack.Children().Append(confirmationQuestionTB);
flyoutStack.Children().Append(acceptBTN);
Flyout acceptChangesFlyout{};
acceptChangesFlyout.Content(flyoutStack);
senderVM.AcceptChangesFlyout(acceptChangesFlyout);
return;
}
}
// update settings model and view model
applyChangesToSettingsModel();
// We NEED to toggle the edit mode here,
// so that if nothing changed, we still exit
// edit mode.
senderVM.ToggleEditMode();
}
void Actions::_ViewModelDeleteNewlyAddedKeyBindingHandler(const Editor::KeyBindingViewModel& senderVM, const IInspectable& /*args*/)
{
for (uint32_t i = 0; i < _KeyBindingList.Size(); ++i)
{
const auto& kbdVM{ _KeyBindingList.GetAt(i) };
if (kbdVM == senderVM)
{
_KeyBindingList.RemoveAt(i);
return;
}
}
}
// Method Description:
// - performs a search on KeyBindingList by key chord.
// Arguments:
// - keys - the associated key chord of the command we're looking for
// Return Value:
// - the index of the view model referencing the command. If the command doesn't exist, nullopt
std::optional<uint32_t> Actions::_GetContainerIndexByKeyChord(const Control::KeyChord& keys)
{
for (uint32_t i = 0; i < _KeyBindingList.Size(); ++i)
{
const auto kbdVM{ get_self<KeyBindingViewModel>(_KeyBindingList.GetAt(i)) };
const auto& otherKeys{ kbdVM->CurrentKeys() };
if (otherKeys && keys.Modifiers() == otherKeys.Modifiers() && keys.Vkey() == otherKeys.Vkey())
{
return i;
}
}
// TODO GH #6900:
// an expedited search can be done if we use cmd.Name()
// to quickly search through the sorted list.
return std::nullopt;
}
void Actions::_RegisterEvents(com_ptr<KeyBindingViewModel>& kbdVM)
{
kbdVM->PropertyChanged({ this, &Actions::_ViewModelPropertyChangedHandler });
kbdVM->DeleteKeyBindingRequested({ this, &Actions::_ViewModelDeleteKeyBindingHandler });
kbdVM->ModifyKeyBindingRequested({ this, &Actions::_ViewModelModifyKeyBindingHandler });
kbdVM->IsAutomationPeerAttached(_AutomationPeerAttached);
}
}

View File

@@ -4,12 +4,109 @@
#pragma once
#include "Actions.g.h"
#include "ActionsViewModel.h"
#include "KeyBindingViewModel.g.h"
#include "ActionsPageNavigationState.g.h"
#include "ModifyKeyBindingEventArgs.g.h"
#include "Utils.h"
#include "ViewModelHelpers.h"
namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
struct KeyBindingViewModelComparator
{
bool operator()(const Editor::KeyBindingViewModel& lhs, const Editor::KeyBindingViewModel& rhs) const
{
return lhs.Name() < rhs.Name();
}
};
struct ModifyKeyBindingEventArgs : ModifyKeyBindingEventArgsT<ModifyKeyBindingEventArgs>
{
public:
ModifyKeyBindingEventArgs(const Control::KeyChord& oldKeys, const Control::KeyChord& newKeys, const hstring oldActionName, const hstring newActionName) :
_OldKeys{ oldKeys },
_NewKeys{ newKeys },
_OldActionName{ std::move(oldActionName) },
_NewActionName{ std::move(newActionName) } {}
WINRT_PROPERTY(Control::KeyChord, OldKeys, nullptr);
WINRT_PROPERTY(Control::KeyChord, NewKeys, nullptr);
WINRT_PROPERTY(hstring, OldActionName);
WINRT_PROPERTY(hstring, NewActionName);
};
struct KeyBindingViewModel : KeyBindingViewModelT<KeyBindingViewModel>, ViewModelHelper<KeyBindingViewModel>
{
public:
KeyBindingViewModel(const Windows::Foundation::Collections::IObservableVector<hstring>& availableActions);
KeyBindingViewModel(const Control::KeyChord& keys, const hstring& name, const Windows::Foundation::Collections::IObservableVector<hstring>& availableActions);
hstring Name() const { return _CurrentAction; }
hstring KeyChordText() const { return _KeyChordText; }
// UIA Text
hstring EditButtonName() const noexcept;
hstring CancelButtonName() const noexcept;
hstring AcceptButtonName() const noexcept;
hstring DeleteButtonName() const noexcept;
void EnterHoverMode() { IsHovered(true); };
void ExitHoverMode() { IsHovered(false); };
void ActionGotFocus() { IsContainerFocused(true); };
void ActionLostFocus() { IsContainerFocused(false); };
void EditButtonGettingFocus() { IsEditButtonFocused(true); };
void EditButtonLosingFocus() { IsEditButtonFocused(false); };
bool ShowEditButton() const noexcept;
void ToggleEditMode();
void DisableEditMode() { IsInEditMode(false); }
void AttemptAcceptChanges();
void AttemptAcceptChanges(const Control::KeyChord newKeys);
void CancelChanges();
void DeleteKeyBinding() { _DeleteKeyBindingRequestedHandlers(*this, _CurrentKeys); }
// ProposedAction: the entry selected by the combo box; may disagree with the settings model.
// CurrentAction: the combo box item that maps to the settings model value.
// AvailableActions: the list of options in the combo box; both actions above must be in this list.
// NOTE: ProposedAction and CurrentAction may disagree mainly due to the "edit mode" system in place.
// Current Action serves as...
// 1 - a record of what to set ProposedAction to on a cancellation
// 2 - a form of translation between ProposedAction and the settings model
// We would also need an ActionMap reference to remove this, but this is a better separation
// of responsibilities.
VIEW_MODEL_OBSERVABLE_PROPERTY(IInspectable, ProposedAction);
VIEW_MODEL_OBSERVABLE_PROPERTY(hstring, CurrentAction);
WINRT_PROPERTY(Windows::Foundation::Collections::IObservableVector<hstring>, AvailableActions, nullptr);
// ProposedKeys: the keys proposed by the control; may disagree with the settings model.
// CurrentKeys: the key chord bound in the settings model.
VIEW_MODEL_OBSERVABLE_PROPERTY(Control::KeyChord, ProposedKeys);
VIEW_MODEL_OBSERVABLE_PROPERTY(Control::KeyChord, CurrentKeys, nullptr);
VIEW_MODEL_OBSERVABLE_PROPERTY(bool, IsInEditMode, false);
VIEW_MODEL_OBSERVABLE_PROPERTY(bool, IsNewlyAdded, false);
VIEW_MODEL_OBSERVABLE_PROPERTY(Windows::UI::Xaml::Controls::Flyout, AcceptChangesFlyout, nullptr);
VIEW_MODEL_OBSERVABLE_PROPERTY(bool, IsAutomationPeerAttached, false);
VIEW_MODEL_OBSERVABLE_PROPERTY(bool, IsHovered, false);
VIEW_MODEL_OBSERVABLE_PROPERTY(bool, IsContainerFocused, false);
VIEW_MODEL_OBSERVABLE_PROPERTY(bool, IsEditButtonFocused, false);
VIEW_MODEL_OBSERVABLE_PROPERTY(Windows::UI::Xaml::Media::Brush, ContainerBackground, nullptr);
TYPED_EVENT(ModifyKeyBindingRequested, Editor::KeyBindingViewModel, Editor::ModifyKeyBindingEventArgs);
TYPED_EVENT(DeleteKeyBindingRequested, Editor::KeyBindingViewModel, Terminal::Control::KeyChord);
TYPED_EVENT(DeleteNewlyAddedKeyBinding, Editor::KeyBindingViewModel, IInspectable);
private:
hstring _KeyChordText{};
};
struct ActionsPageNavigationState : ActionsPageNavigationStateT<ActionsPageNavigationState>
{
public:
ActionsPageNavigationState(const Model::CascadiaSettings& settings) :
_Settings{ settings } {}
WINRT_PROPERTY(Model::CascadiaSettings, Settings, nullptr)
};
struct Actions : public HasScrollViewer<Actions>, ActionsT<Actions>
{
public:
@@ -17,11 +114,24 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
void OnNavigatedTo(const winrt::Windows::UI::Xaml::Navigation::NavigationEventArgs& e);
Windows::UI::Xaml::Automation::Peers::AutomationPeer OnCreateAutomationPeer();
void AddNew_Click(const IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& eventArgs);
WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler);
WINRT_OBSERVABLE_PROPERTY(Editor::ActionsViewModel, ViewModel, _PropertyChangedHandlers, nullptr);
WINRT_PROPERTY(Editor::ActionsPageNavigationState, State, nullptr);
WINRT_PROPERTY(Windows::Foundation::Collections::IObservableVector<Editor::KeyBindingViewModel>, KeyBindingList);
private:
void _ViewModelPropertyChangedHandler(const Windows::Foundation::IInspectable& senderVM, const Windows::UI::Xaml::Data::PropertyChangedEventArgs& args);
void _ViewModelDeleteKeyBindingHandler(const Editor::KeyBindingViewModel& senderVM, const Control::KeyChord& args);
void _ViewModelModifyKeyBindingHandler(const Editor::KeyBindingViewModel& senderVM, const Editor::ModifyKeyBindingEventArgs& args);
void _ViewModelDeleteNewlyAddedKeyBindingHandler(const Editor::KeyBindingViewModel& senderVM, const IInspectable& args);
std::optional<uint32_t> _GetContainerIndexByKeyChord(const Control::KeyChord& keys);
void _RegisterEvents(com_ptr<implementation::KeyBindingViewModel>& kbdVM);
bool _AutomationPeerAttached{ false };
Windows::Foundation::Collections::IObservableVector<hstring> _AvailableActionAndArgs;
Windows::Foundation::Collections::IMap<hstring, Model::ActionAndArgs> _AvailableActionMap;
};
}

View File

@@ -1,13 +1,63 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import "ActionsViewModel.idl";
import "EnumEntry.idl";
namespace Microsoft.Terminal.Settings.Editor
{
runtimeclass ModifyKeyBindingEventArgs
{
Microsoft.Terminal.Control.KeyChord OldKeys { get; };
Microsoft.Terminal.Control.KeyChord NewKeys { get; };
String OldActionName { get; };
String NewActionName { get; };
}
runtimeclass KeyBindingViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged
{
// Settings Model side
String Name { get; };
String KeyChordText { get; };
// UI side
Boolean ShowEditButton { get; };
Boolean IsInEditMode { get; };
Boolean IsNewlyAdded { get; };
Microsoft.Terminal.Control.KeyChord ProposedKeys;
Object ProposedAction;
Windows.UI.Xaml.Controls.Flyout AcceptChangesFlyout;
String EditButtonName { get; };
String CancelButtonName { get; };
String AcceptButtonName { get; };
String DeleteButtonName { get; };
Windows.UI.Xaml.Media.Brush ContainerBackground { get; };
void EnterHoverMode();
void ExitHoverMode();
void ActionGotFocus();
void ActionLostFocus();
void EditButtonGettingFocus();
void EditButtonLosingFocus();
IObservableVector<String> AvailableActions { get; };
void ToggleEditMode();
void AttemptAcceptChanges();
void CancelChanges();
void DeleteKeyBinding();
event Windows.Foundation.TypedEventHandler<KeyBindingViewModel, ModifyKeyBindingEventArgs> ModifyKeyBindingRequested;
event Windows.Foundation.TypedEventHandler<KeyBindingViewModel, Microsoft.Terminal.Control.KeyChord> DeleteKeyBindingRequested;
}
runtimeclass ActionsPageNavigationState
{
Microsoft.Terminal.Settings.Model.CascadiaSettings Settings;
};
[default_interface] runtimeclass Actions : Windows.UI.Xaml.Controls.Page
{
Actions();
ActionsViewModel ViewModel { get; };
ActionsPageNavigationState State { get; };
IObservableVector<KeyBindingViewModel> KeyBindingList { get; };
}
}

View File

@@ -347,7 +347,7 @@
<!-- Keybindings -->
<ListView x:Name="KeyBindingsListView"
ItemTemplate="{StaticResource KeyBindingTemplate}"
ItemsSource="{x:Bind ViewModel.KeyBindingList, Mode=OneWay}"
ItemsSource="{x:Bind KeyBindingList, Mode=OneWay}"
SelectionMode="None" />
</StackPanel>
</Border>

View File

@@ -1,380 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "ActionsViewModel.h"
#include "ActionsViewModel.g.cpp"
#include "KeyBindingViewModel.g.cpp"
#include "LibraryResources.h"
#include "../TerminalSettingsModel/AllShortcutActions.h"
using namespace winrt::Windows::Foundation;
using namespace winrt::Windows::Foundation::Collections;
using namespace winrt::Windows::System;
using namespace winrt::Windows::UI::Core;
using namespace winrt::Windows::UI::Xaml;
using namespace winrt::Windows::UI::Xaml::Controls;
using namespace winrt::Windows::UI::Xaml::Data;
using namespace winrt::Windows::UI::Xaml::Navigation;
using namespace winrt::Microsoft::Terminal::Settings::Model;
namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
KeyBindingViewModel::KeyBindingViewModel(const IObservableVector<hstring>& availableActions) :
KeyBindingViewModel(nullptr, availableActions.First().Current(), availableActions) {}
KeyBindingViewModel::KeyBindingViewModel(const Control::KeyChord& keys, const hstring& actionName, const IObservableVector<hstring>& availableActions) :
_CurrentKeys{ keys },
_KeyChordText{ KeyChordSerialization::ToString(keys) },
_CurrentAction{ actionName },
_ProposedAction{ box_value(actionName) },
_AvailableActions{ availableActions }
{
// Add a property changed handler to our own property changed event.
// This propagates changes from the settings model to anybody listening to our
// unique view model members.
PropertyChanged([this](auto&&, const PropertyChangedEventArgs& args) {
const auto viewModelProperty{ args.PropertyName() };
if (viewModelProperty == L"CurrentKeys")
{
_KeyChordText = KeyChordSerialization::ToString(_CurrentKeys);
_NotifyChanges(L"KeyChordText");
}
else if (viewModelProperty == L"IsContainerFocused" ||
viewModelProperty == L"IsEditButtonFocused" ||
viewModelProperty == L"IsHovered" ||
viewModelProperty == L"IsAutomationPeerAttached" ||
viewModelProperty == L"IsInEditMode")
{
_NotifyChanges(L"ShowEditButton");
}
else if (viewModelProperty == L"CurrentAction")
{
_NotifyChanges(L"Name");
}
});
}
hstring KeyBindingViewModel::EditButtonName() const noexcept { return RS_(L"Actions_EditButton/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip"); }
hstring KeyBindingViewModel::CancelButtonName() const noexcept { return RS_(L"Actions_CancelButton/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip"); }
hstring KeyBindingViewModel::AcceptButtonName() const noexcept { return RS_(L"Actions_AcceptButton/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip"); }
hstring KeyBindingViewModel::DeleteButtonName() const noexcept { return RS_(L"Actions_DeleteButton/[using:Windows.UI.Xaml.Controls]ToolTipService/ToolTip"); }
bool KeyBindingViewModel::ShowEditButton() const noexcept
{
return (IsContainerFocused() || IsEditButtonFocused() || IsHovered() || IsAutomationPeerAttached()) && !IsInEditMode();
}
void KeyBindingViewModel::ToggleEditMode()
{
// toggle edit mode
IsInEditMode(!_IsInEditMode);
if (_IsInEditMode)
{
// if we're in edit mode,
// - pre-populate the text box with the current keys
// - reset the combo box with the current action
ProposedKeys(_CurrentKeys);
ProposedAction(box_value(_CurrentAction));
}
}
void KeyBindingViewModel::AttemptAcceptChanges()
{
AttemptAcceptChanges(_ProposedKeys);
}
void KeyBindingViewModel::AttemptAcceptChanges(const Control::KeyChord newKeys)
{
const auto args{ make_self<ModifyKeyBindingEventArgs>(_CurrentKeys, // OldKeys
newKeys, // NewKeys
_IsNewlyAdded ? hstring{} : _CurrentAction, // OldAction
unbox_value<hstring>(_ProposedAction)) }; // NewAction
_ModifyKeyBindingRequestedHandlers(*this, *args);
}
void KeyBindingViewModel::CancelChanges()
{
if (_IsNewlyAdded)
{
_DeleteNewlyAddedKeyBindingHandlers(*this, nullptr);
}
else
{
ToggleEditMode();
}
}
ActionsViewModel::ActionsViewModel(Model::CascadiaSettings settings) :
_Settings{ settings }
{
// Populate AvailableActionAndArgs
_AvailableActionMap = single_threaded_map<hstring, Model::ActionAndArgs>();
std::vector<hstring> availableActionAndArgs;
for (const auto& [name, actionAndArgs] : _Settings.ActionMap().AvailableActions())
{
availableActionAndArgs.push_back(name);
_AvailableActionMap.Insert(name, actionAndArgs);
}
std::sort(begin(availableActionAndArgs), end(availableActionAndArgs));
_AvailableActionAndArgs = single_threaded_observable_vector(std::move(availableActionAndArgs));
// Convert the key bindings from our settings into a view model representation
const auto& keyBindingMap{ _Settings.ActionMap().KeyBindings() };
std::vector<Editor::KeyBindingViewModel> keyBindingList;
keyBindingList.reserve(keyBindingMap.Size());
for (const auto& [keys, cmd] : keyBindingMap)
{
// convert the cmd into a KeyBindingViewModel
auto container{ make_self<KeyBindingViewModel>(keys, cmd.Name(), _AvailableActionAndArgs) };
_RegisterEvents(container);
keyBindingList.push_back(*container);
}
std::sort(begin(keyBindingList), end(keyBindingList), KeyBindingViewModelComparator{});
_KeyBindingList = single_threaded_observable_vector(std::move(keyBindingList));
}
void ActionsViewModel::OnAutomationPeerAttached()
{
_AutomationPeerAttached = true;
for (const auto& kbdVM : _KeyBindingList)
{
// To create a more accessible experience, we want the "edit" buttons to _always_
// appear when a screen reader is attached. This ensures that the edit buttons are
// accessible via the UIA tree.
get_self<KeyBindingViewModel>(kbdVM)->IsAutomationPeerAttached(_AutomationPeerAttached);
}
}
void ActionsViewModel::AddNewKeybinding()
{
// Create the new key binding and register all of the event handlers.
auto kbdVM{ make_self<KeyBindingViewModel>(_AvailableActionAndArgs) };
_RegisterEvents(kbdVM);
kbdVM->DeleteNewlyAddedKeyBinding({ this, &ActionsViewModel::_KeyBindingViewModelDeleteNewlyAddedKeyBindingHandler });
// Manually add the editing background. This needs to be done in Actions not the view model.
// We also have to do this manually because it hasn't been added to the list yet.
kbdVM->IsInEditMode(true);
// Emit an event to let the page know to update the background of this key binding VM
_UpdateBackgroundHandlers(*this, *kbdVM);
// IMPORTANT: do this _after_ setting IsInEditMode. Otherwise, it'll get deleted immediately
// by the PropertyChangedHandler below (where we delete any IsNewlyAdded items)
kbdVM->IsNewlyAdded(true);
_KeyBindingList.InsertAt(0, *kbdVM);
_FocusContainerHandlers(*this, *kbdVM);
}
void ActionsViewModel::_KeyBindingViewModelPropertyChangedHandler(const IInspectable& sender, const Windows::UI::Xaml::Data::PropertyChangedEventArgs& args)
{
const auto senderVM{ sender.as<Editor::KeyBindingViewModel>() };
const auto propertyName{ args.PropertyName() };
if (propertyName == L"IsInEditMode")
{
if (senderVM.IsInEditMode())
{
// Ensure that...
// 1. we move focus to the edit mode controls
// 2. any actions that were newly added are removed
// 3. this is the only entry that is in edit mode
for (int32_t i = _KeyBindingList.Size() - 1; i >= 0; --i)
{
const auto& kbdVM{ _KeyBindingList.GetAt(i) };
if (senderVM == kbdVM)
{
// This is the view model entry that went into edit mode.
// Emit an event to let the page know to move focus to
// this VM's container.
_FocusContainerHandlers(*this, senderVM);
}
else if (kbdVM.IsNewlyAdded())
{
// Remove any actions that were newly added
_KeyBindingList.RemoveAt(i);
}
else
{
// Exit edit mode for all other containers
get_self<KeyBindingViewModel>(kbdVM)->DisableEditMode();
}
}
}
else
{
// Emit an event to let the page know to move focus to
// this VM's container.
_FocusContainerHandlers(*this, senderVM);
}
// Emit an event to let the page know to update the background of this key binding VM
_UpdateBackgroundHandlers(*this, senderVM);
}
}
void ActionsViewModel::_KeyBindingViewModelDeleteKeyBindingHandler(const Editor::KeyBindingViewModel& senderVM, const Control::KeyChord& keys)
{
// Update the settings model
_Settings.ActionMap().DeleteKeyBinding(keys);
// Find the current container in our list and remove it.
// This is much faster than rebuilding the entire ActionMap.
uint32_t index;
if (_KeyBindingList.IndexOf(senderVM, index))
{
_KeyBindingList.RemoveAt(index);
// Focus the new item at this index
if (_KeyBindingList.Size() != 0)
{
const auto newFocusedIndex{ std::clamp(index, 0u, _KeyBindingList.Size() - 1) };
// Emit an event to let the page know to move focus to
// this VM's container.
_FocusContainerHandlers(*this, winrt::box_value(newFocusedIndex));
}
}
}
void ActionsViewModel::_KeyBindingViewModelModifyKeyBindingHandler(const Editor::KeyBindingViewModel& senderVM, const Editor::ModifyKeyBindingEventArgs& args)
{
const auto isNewAction{ !args.OldKeys() && args.OldActionName().empty() };
auto applyChangesToSettingsModel = [=]() {
// If the key chord was changed,
// update the settings model and view model appropriately
// NOTE: we still need to update the view model if we're working with a newly added action
if (isNewAction || args.OldKeys().Modifiers() != args.NewKeys().Modifiers() || args.OldKeys().Vkey() != args.NewKeys().Vkey())
{
if (!isNewAction)
{
// update settings model
_Settings.ActionMap().RebindKeys(args.OldKeys(), args.NewKeys());
}
// update view model
auto senderVMImpl{ get_self<KeyBindingViewModel>(senderVM) };
senderVMImpl->CurrentKeys(args.NewKeys());
}
// If the action was changed,
// update the settings model and view model appropriately
// NOTE: no need to check for "isNewAction" here. <empty_string> != <action name> already.
if (args.OldActionName() != args.NewActionName())
{
// convert the action's name into a view model.
const auto& newAction{ _AvailableActionMap.Lookup(args.NewActionName()) };
// update settings model
_Settings.ActionMap().RegisterKeyBinding(args.NewKeys(), newAction);
// update view model
auto senderVMImpl{ get_self<KeyBindingViewModel>(senderVM) };
senderVMImpl->CurrentAction(args.NewActionName());
senderVMImpl->IsNewlyAdded(false);
}
};
// Check for this special case:
// we're changing the key chord,
// but the new key chord is already in use
if (isNewAction || args.OldKeys().Modifiers() != args.NewKeys().Modifiers() || args.OldKeys().Vkey() != args.NewKeys().Vkey())
{
const auto& conflictingCmd{ _Settings.ActionMap().GetActionByKeyChord(args.NewKeys()) };
if (conflictingCmd)
{
// We're about to overwrite another key chord.
// Display a confirmation dialog.
TextBlock errorMessageTB{};
errorMessageTB.Text(RS_(L"Actions_RenameConflictConfirmationMessage"));
const auto conflictingCmdName{ conflictingCmd.Name() };
TextBlock conflictingCommandNameTB{};
conflictingCommandNameTB.Text(fmt::format(L"\"{}\"", conflictingCmdName.empty() ? RS_(L"Actions_UnnamedCommandName") : conflictingCmdName));
conflictingCommandNameTB.FontStyle(Windows::UI::Text::FontStyle::Italic);
TextBlock confirmationQuestionTB{};
confirmationQuestionTB.Text(RS_(L"Actions_RenameConflictConfirmationQuestion"));
Button acceptBTN{};
acceptBTN.Content(box_value(RS_(L"Actions_RenameConflictConfirmationAcceptButton")));
acceptBTN.Click([=](auto&, auto&) {
// remove conflicting key binding from list view
const auto containerIndex{ _GetContainerIndexByKeyChord(args.NewKeys()) };
_KeyBindingList.RemoveAt(*containerIndex);
// remove flyout
senderVM.AcceptChangesFlyout().Hide();
senderVM.AcceptChangesFlyout(nullptr);
// update settings model and view model
applyChangesToSettingsModel();
senderVM.ToggleEditMode();
});
StackPanel flyoutStack{};
flyoutStack.Children().Append(errorMessageTB);
flyoutStack.Children().Append(conflictingCommandNameTB);
flyoutStack.Children().Append(confirmationQuestionTB);
flyoutStack.Children().Append(acceptBTN);
Flyout acceptChangesFlyout{};
acceptChangesFlyout.Content(flyoutStack);
senderVM.AcceptChangesFlyout(acceptChangesFlyout);
}
}
// update settings model and view model
applyChangesToSettingsModel();
// We NEED to toggle the edit mode here,
// so that if nothing changed, we still exit
// edit mode.
senderVM.ToggleEditMode();
}
void ActionsViewModel::_KeyBindingViewModelDeleteNewlyAddedKeyBindingHandler(const Editor::KeyBindingViewModel& senderVM, const IInspectable& /*args*/)
{
for (uint32_t i = 0; i < _KeyBindingList.Size(); ++i)
{
const auto& kbdVM{ _KeyBindingList.GetAt(i) };
if (kbdVM == senderVM)
{
_KeyBindingList.RemoveAt(i);
return;
}
}
}
// Method Description:
// - performs a search on KeyBindingList by key chord.
// Arguments:
// - keys - the associated key chord of the command we're looking for
// Return Value:
// - the index of the view model referencing the command. If the command doesn't exist, nullopt
std::optional<uint32_t> ActionsViewModel::_GetContainerIndexByKeyChord(const Control::KeyChord& keys)
{
for (uint32_t i = 0; i < _KeyBindingList.Size(); ++i)
{
const auto kbdVM{ get_self<KeyBindingViewModel>(_KeyBindingList.GetAt(i)) };
const auto& otherKeys{ kbdVM->CurrentKeys() };
if (otherKeys && keys.Modifiers() == otherKeys.Modifiers() && keys.Vkey() == otherKeys.Vkey())
{
return i;
}
}
// TODO GH #6900:
// an expedited search can be done if we use cmd.Name()
// to quickly search through the sorted list.
return std::nullopt;
}
void ActionsViewModel::_RegisterEvents(com_ptr<KeyBindingViewModel>& kbdVM)
{
kbdVM->PropertyChanged({ this, &ActionsViewModel::_KeyBindingViewModelPropertyChangedHandler });
kbdVM->DeleteKeyBindingRequested({ this, &ActionsViewModel::_KeyBindingViewModelDeleteKeyBindingHandler });
kbdVM->ModifyKeyBindingRequested({ this, &ActionsViewModel::_KeyBindingViewModelModifyKeyBindingHandler });
kbdVM->IsAutomationPeerAttached(_AutomationPeerAttached);
}
}

View File

@@ -1,131 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include "ActionsViewModel.g.h"
#include "KeyBindingViewModel.g.h"
#include "ModifyKeyBindingEventArgs.g.h"
#include "Utils.h"
#include "ViewModelHelpers.h"
namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
struct KeyBindingViewModelComparator
{
bool operator()(const Editor::KeyBindingViewModel& lhs, const Editor::KeyBindingViewModel& rhs) const
{
return lhs.Name() < rhs.Name();
}
};
struct ModifyKeyBindingEventArgs : ModifyKeyBindingEventArgsT<ModifyKeyBindingEventArgs>
{
public:
ModifyKeyBindingEventArgs(const Control::KeyChord& oldKeys, const Control::KeyChord& newKeys, const hstring oldActionName, const hstring newActionName) :
_OldKeys{ oldKeys },
_NewKeys{ newKeys },
_OldActionName{ std::move(oldActionName) },
_NewActionName{ std::move(newActionName) } {}
WINRT_PROPERTY(Control::KeyChord, OldKeys, nullptr);
WINRT_PROPERTY(Control::KeyChord, NewKeys, nullptr);
WINRT_PROPERTY(hstring, OldActionName);
WINRT_PROPERTY(hstring, NewActionName);
};
struct KeyBindingViewModel : KeyBindingViewModelT<KeyBindingViewModel>, ViewModelHelper<KeyBindingViewModel>
{
public:
KeyBindingViewModel(const Windows::Foundation::Collections::IObservableVector<hstring>& availableActions);
KeyBindingViewModel(const Control::KeyChord& keys, const hstring& name, const Windows::Foundation::Collections::IObservableVector<hstring>& availableActions);
hstring Name() const { return _CurrentAction; }
hstring KeyChordText() const { return _KeyChordText; }
// UIA Text
hstring EditButtonName() const noexcept;
hstring CancelButtonName() const noexcept;
hstring AcceptButtonName() const noexcept;
hstring DeleteButtonName() const noexcept;
void EnterHoverMode() { IsHovered(true); };
void ExitHoverMode() { IsHovered(false); };
void ActionGotFocus() { IsContainerFocused(true); };
void ActionLostFocus() { IsContainerFocused(false); };
void EditButtonGettingFocus() { IsEditButtonFocused(true); };
void EditButtonLosingFocus() { IsEditButtonFocused(false); };
bool ShowEditButton() const noexcept;
void ToggleEditMode();
void DisableEditMode() { IsInEditMode(false); }
void AttemptAcceptChanges();
void AttemptAcceptChanges(const Control::KeyChord newKeys);
void CancelChanges();
void DeleteKeyBinding() { _DeleteKeyBindingRequestedHandlers(*this, _CurrentKeys); }
// ProposedAction: the entry selected by the combo box; may disagree with the settings model.
// CurrentAction: the combo box item that maps to the settings model value.
// AvailableActions: the list of options in the combo box; both actions above must be in this list.
// NOTE: ProposedAction and CurrentAction may disagree mainly due to the "edit mode" system in place.
// Current Action serves as...
// 1 - a record of what to set ProposedAction to on a cancellation
// 2 - a form of translation between ProposedAction and the settings model
// We would also need an ActionMap reference to remove this, but this is a better separation
// of responsibilities.
VIEW_MODEL_OBSERVABLE_PROPERTY(IInspectable, ProposedAction);
VIEW_MODEL_OBSERVABLE_PROPERTY(hstring, CurrentAction);
WINRT_PROPERTY(Windows::Foundation::Collections::IObservableVector<hstring>, AvailableActions, nullptr);
// ProposedKeys: the keys proposed by the control; may disagree with the settings model.
// CurrentKeys: the key chord bound in the settings model.
VIEW_MODEL_OBSERVABLE_PROPERTY(Control::KeyChord, ProposedKeys);
VIEW_MODEL_OBSERVABLE_PROPERTY(Control::KeyChord, CurrentKeys, nullptr);
VIEW_MODEL_OBSERVABLE_PROPERTY(bool, IsInEditMode, false);
VIEW_MODEL_OBSERVABLE_PROPERTY(bool, IsNewlyAdded, false);
VIEW_MODEL_OBSERVABLE_PROPERTY(Windows::UI::Xaml::Controls::Flyout, AcceptChangesFlyout, nullptr);
VIEW_MODEL_OBSERVABLE_PROPERTY(bool, IsAutomationPeerAttached, false);
VIEW_MODEL_OBSERVABLE_PROPERTY(bool, IsHovered, false);
VIEW_MODEL_OBSERVABLE_PROPERTY(bool, IsContainerFocused, false);
VIEW_MODEL_OBSERVABLE_PROPERTY(bool, IsEditButtonFocused, false);
VIEW_MODEL_OBSERVABLE_PROPERTY(Windows::UI::Xaml::Media::Brush, ContainerBackground, nullptr);
TYPED_EVENT(ModifyKeyBindingRequested, Editor::KeyBindingViewModel, Editor::ModifyKeyBindingEventArgs);
TYPED_EVENT(DeleteKeyBindingRequested, Editor::KeyBindingViewModel, Terminal::Control::KeyChord);
TYPED_EVENT(DeleteNewlyAddedKeyBinding, Editor::KeyBindingViewModel, IInspectable);
private:
hstring _KeyChordText{};
};
struct ActionsViewModel : ActionsViewModelT<ActionsViewModel>, ViewModelHelper<ActionsViewModel>
{
public:
ActionsViewModel(Model::CascadiaSettings settings);
void OnAutomationPeerAttached();
void AddNewKeybinding();
WINRT_PROPERTY(Windows::Foundation::Collections::IObservableVector<Editor::KeyBindingViewModel>, KeyBindingList);
TYPED_EVENT(FocusContainer, IInspectable, IInspectable);
TYPED_EVENT(UpdateBackground, IInspectable, IInspectable);
private:
bool _AutomationPeerAttached{ false };
Model::CascadiaSettings _Settings;
Windows::Foundation::Collections::IObservableVector<hstring> _AvailableActionAndArgs;
Windows::Foundation::Collections::IMap<hstring, Model::ActionAndArgs> _AvailableActionMap;
std::optional<uint32_t> _GetContainerIndexByKeyChord(const Control::KeyChord& keys);
void _RegisterEvents(com_ptr<implementation::KeyBindingViewModel>& kbdVM);
void _KeyBindingViewModelPropertyChangedHandler(const Windows::Foundation::IInspectable& senderVM, const Windows::UI::Xaml::Data::PropertyChangedEventArgs& args);
void _KeyBindingViewModelDeleteKeyBindingHandler(const Editor::KeyBindingViewModel& senderVM, const Control::KeyChord& args);
void _KeyBindingViewModelModifyKeyBindingHandler(const Editor::KeyBindingViewModel& senderVM, const Editor::ModifyKeyBindingEventArgs& args);
void _KeyBindingViewModelDeleteNewlyAddedKeyBindingHandler(const Editor::KeyBindingViewModel& senderVM, const IInspectable& args);
};
}
namespace winrt::Microsoft::Terminal::Settings::Editor::factory_implementation
{
BASIC_FACTORY(ActionsViewModel);
}

View File

@@ -1,60 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
namespace Microsoft.Terminal.Settings.Editor
{
runtimeclass ModifyKeyBindingEventArgs
{
Microsoft.Terminal.Control.KeyChord OldKeys { get; };
Microsoft.Terminal.Control.KeyChord NewKeys { get; };
String OldActionName { get; };
String NewActionName { get; };
}
runtimeclass KeyBindingViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged
{
// Settings Model side
String Name { get; };
String KeyChordText { get; };
// UI side
Boolean ShowEditButton { get; };
Boolean IsInEditMode { get; };
Boolean IsNewlyAdded { get; };
Microsoft.Terminal.Control.KeyChord ProposedKeys;
Object ProposedAction;
Windows.UI.Xaml.Controls.Flyout AcceptChangesFlyout;
String EditButtonName { get; };
String CancelButtonName { get; };
String AcceptButtonName { get; };
String DeleteButtonName { get; };
Windows.UI.Xaml.Media.Brush ContainerBackground { get; };
void EnterHoverMode();
void ExitHoverMode();
void ActionGotFocus();
void ActionLostFocus();
void EditButtonGettingFocus();
void EditButtonLosingFocus();
IObservableVector<String> AvailableActions { get; };
void ToggleEditMode();
void AttemptAcceptChanges();
void CancelChanges();
void DeleteKeyBinding();
event Windows.Foundation.TypedEventHandler<KeyBindingViewModel, ModifyKeyBindingEventArgs> ModifyKeyBindingRequested;
event Windows.Foundation.TypedEventHandler<KeyBindingViewModel, Microsoft.Terminal.Control.KeyChord> DeleteKeyBindingRequested;
}
runtimeclass ActionsViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged
{
ActionsViewModel(Microsoft.Terminal.Settings.Model.CascadiaSettings settings);
void OnAutomationPeerAttached();
void AddNewKeybinding();
IObservableVector<KeyBindingViewModel> KeyBindingList { get; };
event Windows.Foundation.TypedEventHandler<Object, Object> FocusContainer;
event Windows.Foundation.TypedEventHandler<Object, Object> UpdateBackground;
}
}

View File

@@ -374,7 +374,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
}
else if (clickedItemTag == actionsTag)
{
contentFrame().Navigate(xaml_typename<Editor::Actions>(), winrt::make<ActionsViewModel>(_settingsClone));
contentFrame().Navigate(xaml_typename<Editor::Actions>(), winrt::make<ActionsPageNavigationState>(_settingsClone));
const auto crumb = winrt::make<Breadcrumb>(box_value(clickedItemTag), RS_(L"Nav_Actions/Content"), BreadcrumbSubPage::None);
_breadcrumbs.Append(crumb);
}

View File

@@ -82,10 +82,6 @@
<DependentUpon>ProfileViewModel.idl</DependentUpon>
<SubType>Code</SubType>
</ClInclude>
<ClInclude Include="ActionsViewModel.h">
<DependentUpon>ActionsViewModel.idl</DependentUpon>
<SubType>Code</SubType>
</ClInclude>
<ClInclude Include="ColorSchemeViewModel.h">
<DependentUpon>ColorSchemeViewModel.idl</DependentUpon>
<SubType>Code</SubType>
@@ -229,10 +225,6 @@
<DependentUpon>ProfileViewModel.idl</DependentUpon>
<SubType>Code</SubType>
</ClCompile>
<ClCompile Include="ActionsViewModel.cpp">
<DependentUpon>ActionsViewModel.idl</DependentUpon>
<SubType>Code</SubType>
</ClCompile>
<ClCompile Include="ColorSchemeViewModel.cpp">
<DependentUpon>ColorSchemeViewModel.idl</DependentUpon>
<SubType>Code</SubType>
@@ -328,7 +320,6 @@
<DependentUpon>MainPage.xaml</DependentUpon>
</Midl>
<Midl Include="ProfileViewModel.idl" />
<Midl Include="ActionsViewModel.idl" />
<Midl Include="ColorSchemeViewModel.idl" />
<Midl Include="ColorSchemesPageViewModel.idl" />
<Midl Include="RenderingViewModel.idl" />

View File

@@ -18,7 +18,6 @@
</ItemGroup>
<ItemGroup>
<Midl Include="ProfileViewModel.idl" />
<Midl Include="ActionsViewModel.idl" />
<Midl Include="ColorSchemeViewModel.idl" />
<Midl Include="ColorSchemesPageViewModel.idl" />
<Midl Include="RenderingViewModel.idl" />

View File

@@ -29,7 +29,6 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
void DisplayPowerlineGlyphs(bool d) noexcept;
winrt::guid SessionId() const noexcept { return {}; }
winrt::Microsoft::Terminal::TerminalConnection::ConnectionState State() const noexcept { return winrt::Microsoft::Terminal::TerminalConnection::ConnectionState::Connected; }
WINRT_CALLBACK(TerminalOutput, winrt::Microsoft::Terminal::TerminalConnection::TerminalOutputHandler);

View File

@@ -102,6 +102,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
OBSERVABLE_PROJECTED_SETTING(_profile, SnapOnInput);
OBSERVABLE_PROJECTED_SETTING(_profile, AltGrAliasing);
OBSERVABLE_PROJECTED_SETTING(_profile, BellStyle);
OBSERVABLE_PROJECTED_SETTING(_profile, UseAtlasEngine);
OBSERVABLE_PROJECTED_SETTING(_profile, Elevate);
OBSERVABLE_PROJECTED_SETTING(_profile, VtPassthrough);
OBSERVABLE_PROJECTED_SETTING(_profile, ReloadEnvironmentVariables);
@@ -116,7 +117,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
private:
Model::Profile _profile;
winrt::guid _originalProfileGuid{};
winrt::guid _originalProfileGuid;
winrt::hstring _lastBgImagePath;
winrt::hstring _lastStartingDirectoryPath;
Editor::AppearanceViewModel _defaultAppearanceViewModel;

View File

@@ -103,6 +103,7 @@ namespace Microsoft.Terminal.Settings.Editor
OBSERVABLE_PROJECTED_PROFILE_SETTING(Boolean, SnapOnInput);
OBSERVABLE_PROJECTED_PROFILE_SETTING(Boolean, AltGrAliasing);
OBSERVABLE_PROJECTED_PROFILE_SETTING(Microsoft.Terminal.Settings.Model.BellStyle, BellStyle);
OBSERVABLE_PROJECTED_PROFILE_SETTING(Boolean, UseAtlasEngine);
OBSERVABLE_PROJECTED_PROFILE_SETTING(Boolean, Elevate);
OBSERVABLE_PROJECTED_PROFILE_SETTING(Boolean, VtPassthrough);
OBSERVABLE_PROJECTED_PROFILE_SETTING(Boolean, ReloadEnvironmentVariables);

View File

@@ -35,8 +35,7 @@
Margin="{StaticResource StandardIndentMargin}"
Style="{StaticResource DisclaimerStyle}"
Visibility="{x:Bind Profile.IsBaseLayer}" />
<StackPanel Grid.Row="1"
Style="{StaticResource SettingsStackStyle}">
<StackPanel Style="{StaticResource SettingsStackStyle}">
<!-- Suppress Application Title -->
<local:SettingContainer x:Uid="Profile_SuppressApplicationTitle"
ClearSettingValue="{x:Bind Profile.ClearSuppressApplicationTitle}"
@@ -117,6 +116,15 @@
</StackPanel>
</local:SettingContainer>
<!-- AtlasEngine -->
<local:SettingContainer x:Uid="Profile_UseAtlasEngine"
ClearSettingValue="{x:Bind Profile.ClearUseAtlasEngine}"
HasSettingValue="{x:Bind Profile.HasUseAtlasEngine, Mode=OneWay}"
SettingOverrideSource="{x:Bind Profile.UseAtlasEngineOverrideSource, Mode=OneWay}">
<ToggleSwitch IsOn="{x:Bind Profile.UseAtlasEngine, Mode=TwoWay}"
Style="{StaticResource ToggleSwitchInExpanderStyle}" />
</local:SettingContainer>
<!-- VtPassthrough -->
<local:SettingContainer x:Uid="Profile_VtPassthrough"
ClearSettingValue="{x:Bind Profile.ClearVtPassthrough}"

View File

@@ -41,14 +41,13 @@
Margin="{StaticResource StandardIndentMargin}"
Style="{StaticResource DisclaimerStyle}"
Visibility="{x:Bind Profile.IsBaseLayer}" />
<StackPanel Grid.Row="1"
Style="{StaticResource SettingsStackStyle}">
<StackPanel Style="{StaticResource SettingsStackStyle}">
<!-- Control Preview -->
<Border MaxWidth="{StaticResource StandardControlMaxWidth}">
<Border x:Name="ControlPreview"
Width="400"
Height="180"
Margin="0,12,0,12"
Margin="0,0,0,12"
HorizontalAlignment="Left"
BorderBrush="{ThemeResource SystemControlForegroundBaseMediumLowBrush}"
BorderThickness="1" />

View File

@@ -31,8 +31,7 @@
Margin="{StaticResource StandardIndentMargin}"
Style="{StaticResource DisclaimerStyle}"
Visibility="{x:Bind Profile.IsBaseLayer}" />
<StackPanel Grid.Row="1"
Style="{StaticResource SettingsStackStyle}">
<StackPanel Style="{StaticResource SettingsStackStyle}">
<!-- Name -->
<!--

View File

@@ -22,6 +22,12 @@
<TextBlock x:Uid="Globals_RenderingDisclaimer"
Style="{StaticResource DisclaimerStyle}" />
<!-- AtlasEngine -->
<local:SettingContainer x:Uid="Profile_UseAtlasEngine">
<ToggleSwitch IsOn="{x:Bind ViewModel.UseAtlasEngine, Mode=TwoWay}"
Style="{StaticResource ToggleSwitchInExpanderStyle}" />
</local:SettingContainer>
<!-- Force Full Repaint -->
<local:SettingContainer x:Uid="Globals_ForceFullRepaint">
<ToggleSwitch IsOn="{x:Bind ViewModel.ForceFullRepaintRendering, Mode=TwoWay}"

View File

@@ -12,6 +12,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
explicit RenderingViewModel(Model::CascadiaSettings settings) noexcept;
PERMANENT_OBSERVABLE_PROJECTED_SETTING(_settings.ProfileDefaults(), UseAtlasEngine);
PERMANENT_OBSERVABLE_PROJECTED_SETTING(_settings.GlobalSettings(), ForceFullRepaintRendering);
PERMANENT_OBSERVABLE_PROJECTED_SETTING(_settings.GlobalSettings(), SoftwareRendering);

View File

@@ -11,6 +11,7 @@ namespace Microsoft.Terminal.Settings.Editor
{
RenderingViewModel(Microsoft.Terminal.Settings.Model.CascadiaSettings settings);
PERMANENT_OBSERVABLE_PROJECTED_SETTING(Boolean, UseAtlasEngine);
PERMANENT_OBSERVABLE_PROJECTED_SETTING(Boolean, ForceFullRepaintRendering);
PERMANENT_OBSERVABLE_PROJECTED_SETTING(Boolean, SoftwareRendering);
}

View File

@@ -1138,6 +1138,10 @@
<value>Controls what happens when the application emits a BEL character.</value>
<comment>A description for what the "bell style" setting does. Presented near "Profile_BellStyle".{Locked="BEL"}</comment>
</data>
<data name="Profile_UseAtlasEngine.Header" xml:space="preserve">
<value>Use the new text renderer ("AtlasEngine")</value>
<comment>{Locked="AtlasEngine"}</comment>
</data>
<data name="Profile_ReloadEnvVars.Header" xml:space="preserve">
<value>Launch this application with a new environment block</value>
<comment>"environment variables" are user-definable values that can affect the way running processes will behave on a computer</comment>

View File

@@ -102,14 +102,34 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
// The destructor ensures that the last write is flushed to disk before returning.
ApplicationState::~ApplicationState()
{
Flush();
}
TraceLoggingWrite(g_hSettingsModelProvider,
"ApplicationState_Dtor_Start",
TraceLoggingDescription("Event at the start of the ApplicationState destructor"),
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
TraceLoggingKeyword(TIL_KEYWORD_TRACE));
void ApplicationState::Flush()
{
// This will ensure that we not just cancel the last outstanding timer,
// but instead force it to run as soon as possible and wait for it to complete.
_throttler.flush();
TraceLoggingWrite(g_hSettingsModelProvider,
"ApplicationState_Dtor_End",
TraceLoggingDescription("Event at the end of the ApplicationState destructor"),
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
TraceLoggingKeyword(TIL_KEYWORD_TRACE));
}
// Re-read the state.json from disk.
void ApplicationState::Reload() const noexcept
{
_read();
}
bool ApplicationState::IsStatePath(const winrt::hstring& filename)
{
static const auto sharedPath{ _sharedPath.filename() };
static const auto elevatedPath{ _elevatedPath.filename() };
return filename == sharedPath || filename == elevatedPath;
}
// Method Description:
@@ -279,7 +299,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
Json::Value ApplicationState::_toJsonWithBlob(Json::Value& root, FileSource parseSource) const noexcept
{
{
const auto state = _state.lock_shared();
auto state = _state.lock_shared();
// GH#11222: We only write properties that are of the same type (Local
// or Shared) which we requested. If we didn't want to serialize this
@@ -306,7 +326,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
void ApplicationState::name(const type& value) noexcept \
{ \
{ \
const auto state = _state.lock(); \
auto state = _state.lock(); \
state->name.emplace(value); \
} \
\

View File

@@ -63,12 +63,15 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
~ApplicationState();
// Methods
void Flush();
void Reload() const noexcept;
void Reset() noexcept;
void FromJson(const Json::Value& root, FileSource parseSource) const noexcept;
Json::Value ToJson(FileSource parseSource) const noexcept;
// General getters/setters
bool IsStatePath(const winrt::hstring& filename);
// State getters/setters
#define MTSM_APPLICATION_STATE_GEN(source, type, name, key, ...) \
type name() const noexcept; \

View File

@@ -28,9 +28,11 @@ namespace Microsoft.Terminal.Settings.Model
[default_interface] runtimeclass ApplicationState {
static ApplicationState SharedInstance();
void Flush();
void Reload();
void Reset();
Boolean IsStatePath(String filename);
String SettingsHash;
Windows.Foundation.Collections.IVector<WindowLayout> PersistedWindowLayouts;
Windows.Foundation.Collections.IVector<String> RecentCommands;

View File

@@ -410,7 +410,7 @@ bool SettingsLoader::FixupUserSettings()
{
struct CommandlinePatch
{
winrt::guid guid{};
winrt::guid guid;
std::wstring_view before;
std::wstring_view after;
};

View File

@@ -83,7 +83,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
static constexpr bool debugFeaturesDefault{ true };
#endif
winrt::guid _defaultProfile{};
winrt::guid _defaultProfile;
bool _legacyReloadEnvironmentVariables{ true };
winrt::com_ptr<implementation::ActionMap> _actionMap{ winrt::make_self<implementation::ActionMap>() };

View File

@@ -90,6 +90,7 @@ Author(s):
X(hstring, TabTitle, "tabTitle") \
X(Model::BellStyle, BellStyle, "bellStyle", BellStyle::Audible) \
X(IEnvironmentVariableMap, EnvironmentVariables, "environment", nullptr) \
X(bool, UseAtlasEngine, "useAtlasEngine", true) \
X(bool, RightClickContextMenu, "experimental.rightClickContextMenu", false) \
X(Windows::Foundation::Collections::IVector<winrt::hstring>, BellSound, "bellSound", nullptr) \
X(bool, Elevate, "elevate", false) \

View File

@@ -85,6 +85,7 @@ namespace Microsoft.Terminal.Settings.Model
INHERITABLE_PROFILE_SETTING(Windows.Foundation.Collections.IMap<String COMMA String>, EnvironmentVariables);
INHERITABLE_PROFILE_SETTING(Boolean, UseAtlasEngine);
INHERITABLE_PROFILE_SETTING(Windows.Foundation.Collections.IVector<String>, BellSound);
INHERITABLE_PROFILE_SETTING(Boolean, Elevate);

View File

@@ -307,6 +307,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
_SuppressApplicationTitle = profile.SuppressApplicationTitle();
}
_UseAtlasEngine = profile.UseAtlasEngine();
_ScrollState = profile.ScrollState();
_AntialiasingMode = profile.AntialiasingMode();
@@ -355,7 +356,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
_WordDelimiters = globalSettings.WordDelimiters();
_CopyOnSelect = globalSettings.CopyOnSelect();
_CopyFormatting = globalSettings.CopyFormatting();
_FocusFollowMouse = globalSettings.FocusFollowMouse();
_ForceFullRepaintRendering = globalSettings.ForceFullRepaintRendering();
_SoftwareRendering = globalSettings.SoftwareRendering();

View File

@@ -91,7 +91,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
INHERITABLE_SETTING(Model::TerminalSettings, uint32_t, CursorHeight, DEFAULT_CURSOR_HEIGHT);
INHERITABLE_SETTING(Model::TerminalSettings, hstring, WordDelimiters, DEFAULT_WORD_DELIMITERS);
INHERITABLE_SETTING(Model::TerminalSettings, bool, CopyOnSelect, false);
INHERITABLE_SETTING(Model::TerminalSettings, Microsoft::Terminal::Control::CopyFormat, CopyFormatting, 0);
INHERITABLE_SETTING(Model::TerminalSettings, bool, FocusFollowMouse, false);
INHERITABLE_SETTING(Model::TerminalSettings, bool, TrimBlockSelection, true);
INHERITABLE_SETTING(Model::TerminalSettings, bool, DetectURLs, true);
@@ -149,6 +148,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
INHERITABLE_SETTING(Model::TerminalSettings, IEnvironmentVariableMap, EnvironmentVariables);
INHERITABLE_SETTING(Model::TerminalSettings, Microsoft::Terminal::Control::ScrollbarState, ScrollState, Microsoft::Terminal::Control::ScrollbarState::Visible);
INHERITABLE_SETTING(Model::TerminalSettings, bool, UseAtlasEngine, true);
INHERITABLE_SETTING(Model::TerminalSettings, Microsoft::Terminal::Control::TextAntialiasingMode, AntialiasingMode, Microsoft::Terminal::Control::TextAntialiasingMode::Grayscale);

View File

@@ -40,6 +40,7 @@
<ProjectReference Include="$(OpenConsoleDir)src\buffer\out\lib\bufferout.vcxproj" />
<ProjectReference Include="$(OpenConsoleDir)src\renderer\base\lib\base.vcxproj" />
<ProjectReference Include="$(OpenConsoleDir)src\renderer\dx\lib\dx.vcxproj" />
<ProjectReference Include="$(OpenConsoleDir)src\renderer\uia\lib\uia.vcxproj" />
<ProjectReference Include="$(OpenConsoleDir)src\terminal\parser\lib\parser.vcxproj" />
<ProjectReference Include="$(OpenConsoleDir)src\terminal\input\lib\terminalinput.vcxproj" />

View File

@@ -23,7 +23,6 @@ namespace ControlUnitTests
void Resize(uint32_t /*rows*/, uint32_t /*columns*/) noexcept {}
void Close() noexcept {}
winrt::guid SessionId() const noexcept { return {}; }
winrt::Microsoft::Terminal::TerminalConnection::ConnectionState State() const noexcept { return winrt::Microsoft::Terminal::TerminalConnection::ConnectionState::Connected; }
WINRT_CALLBACK(TerminalOutput, winrt::Microsoft::Terminal::TerminalConnection::TerminalOutputHandler);

View File

@@ -2,10 +2,18 @@
// Licensed under the MIT license.
#include "pch.h"
#include <WexTestClass.h>
#include <DefaultSettings.h>
#include "../renderer/inc/DummyRenderer.hpp"
#include "../renderer/base/Renderer.hpp"
#include "../renderer/dx/DxRenderer.hpp"
#include "../cascadia/TerminalCore/Terminal.hpp"
#include "../renderer/inc/DummyRenderer.hpp"
#include "../renderer/inc/RenderEngineBase.hpp"
#include "MockTermSettings.h"
#include "consoletaeftemplates.hpp"
#include "../../inc/TestUtils.h"
using namespace winrt::Microsoft::Terminal::Core;
using namespace Microsoft::Terminal::Core;

View File

@@ -8,6 +8,7 @@
#include "../renderer/inc/DummyRenderer.hpp"
#include "../renderer/base/Renderer.hpp"
#include "../renderer/dx/DxRenderer.hpp"
#include "../cascadia/TerminalCore/Terminal.hpp"
#include "MockTermSettings.h"

Some files were not shown because too many files have changed in this diff Show More