mirror of
https://github.com/microsoft/terminal.git
synced 2026-04-07 06:39:44 +00:00
Compare commits
1 Commits
dev/migrie
...
dev/lhecke
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6126532e71 |
5
.github/actions/spelling/allow/allow.txt
vendored
5
.github/actions/spelling/allow/allow.txt
vendored
@@ -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
|
||||
|
||||
2
.github/actions/spelling/allow/apis.txt
vendored
2
.github/actions/spelling/allow/apis.txt
vendored
@@ -4,7 +4,6 @@ acl
|
||||
aclapi
|
||||
alignas
|
||||
alignof
|
||||
allocconsolewithoptions
|
||||
APPLYTOSUBMENUS
|
||||
appxrecipe
|
||||
bitfield
|
||||
@@ -157,7 +156,6 @@ OUTLINETEXTMETRICW
|
||||
overridable
|
||||
PACL
|
||||
PAGESCROLL
|
||||
PALLOC
|
||||
PATINVERT
|
||||
PEXPLICIT
|
||||
PICKFOLDERS
|
||||
|
||||
3
.github/actions/spelling/allow/microsoft.txt
vendored
3
.github/actions/spelling/allow/microsoft.txt
vendored
@@ -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
|
||||
|
||||
1
.github/actions/spelling/excludes.txt
vendored
1
.github/actions/spelling/excludes.txt
vendored
@@ -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$
|
||||
|
||||
14
.github/actions/spelling/expect/expect.txt
vendored
14
.github/actions/spelling/expect/expect.txt
vendored
@@ -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
|
||||
|
||||
444
OpenConsole.sln
444
OpenConsole.sln
File diff suppressed because it is too large
Load Diff
@@ -43,7 +43,7 @@ jobs:
|
||||
|
||||
- template: steps-ensure-nuget-version.yml
|
||||
|
||||
- task: NuGetAuthenticate@1
|
||||
- task: NuGetAuthenticate@0
|
||||
inputs:
|
||||
nuGetServiceConnections: 'Terminal Public Artifact Feed'
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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!
|
||||
@@ -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
|
||||
|
||||

|
||||
|
||||
> **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
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
### 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
|
||||
|
||||
|
||||

|
||||
|
||||
_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 |
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 += "<";
|
||||
break;
|
||||
case '>':
|
||||
htmlBuilder += ">";
|
||||
break;
|
||||
case '&':
|
||||
htmlBuilder += "&";
|
||||
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 << "<";
|
||||
break;
|
||||
case '>':
|
||||
htmlBuilder << ">";
|
||||
break;
|
||||
case '&':
|
||||
htmlBuilder << "&";
|
||||
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)) << "?";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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" ");
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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{};
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#include "pch.h"
|
||||
#include "ColorHelper.h"
|
||||
#include <limits>
|
||||
|
||||
using namespace winrt::TerminalApp;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
#include "pch.h"
|
||||
|
||||
#include <winrt/Windows.UI.h>
|
||||
#include <winrt/windows.ui.core.h>
|
||||
|
||||
namespace winrt::TerminalApp
|
||||
{
|
||||
|
||||
@@ -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() })
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -34,5 +34,5 @@ public:
|
||||
|
||||
private:
|
||||
winrt::Microsoft::UI::Xaml::Controls::TeachingTip _tip;
|
||||
SafeDispatcherTimer _timer;
|
||||
winrt::Windows::UI::Xaml::DispatcherTimer _timer;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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));
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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; };
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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; };
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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; };
|
||||
|
||||
@@ -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; };
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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; };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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 -->
|
||||
<!--
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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); \
|
||||
} \
|
||||
\
|
||||
|
||||
@@ -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; \
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -410,7 +410,7 @@ bool SettingsLoader::FixupUserSettings()
|
||||
{
|
||||
struct CommandlinePatch
|
||||
{
|
||||
winrt::guid guid{};
|
||||
winrt::guid guid;
|
||||
std::wstring_view before;
|
||||
std::wstring_view after;
|
||||
};
|
||||
|
||||
@@ -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>() };
|
||||
|
||||
|
||||
@@ -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) \
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user