diff --git a/.github/actions/spelling/allow/allow.txt b/.github/actions/spelling/allow/allow.txt
index 2e1dd378568..843a1ae91e6 100644
--- a/.github/actions/spelling/allow/allow.txt
+++ b/.github/actions/spelling/allow/allow.txt
@@ -78,6 +78,8 @@ ok'd
overlined
pipeline
postmodern
+Powerline
+powerline
ptys
qof
qps
diff --git a/.github/actions/spelling/allow/apis.txt b/.github/actions/spelling/allow/apis.txt
index 38ef5c30d01..7876e706b19 100644
--- a/.github/actions/spelling/allow/apis.txt
+++ b/.github/actions/spelling/allow/apis.txt
@@ -29,6 +29,7 @@ Dacl
dataobject
dcomp
DERR
+delayimp
dlldata
DNE
DONTADDTORECENT
@@ -75,6 +76,7 @@ IConnection
ICustom
IDialog
IDirect
+Idn
IExplorer
IFACEMETHOD
IFile
@@ -86,6 +88,7 @@ IObject
iosfwd
IPackage
IPeasant
+isa
ISetup
isspace
IStorage
@@ -165,6 +168,7 @@ roundf
RSHIFT
SACL
schandle
+SEH
semver
serializer
SETVERSION
@@ -212,6 +216,7 @@ UOI
UPDATEINIFILE
userenv
USEROBJECTFLAGS
+Vcpp
Viewbox
virtualalloc
vsnwprintf
diff --git a/.github/actions/spelling/candidate.patterns b/.github/actions/spelling/candidate.patterns
index 4b40e728ee3..400f103ea28 100644
--- a/.github/actions/spelling/candidate.patterns
+++ b/.github/actions/spelling/candidate.patterns
@@ -371,8 +371,6 @@ ipfs://[0-9a-z]*
\b(?:[0-9a-fA-F]{0,4}:){3,7}[0-9a-fA-F]{0,4}\b
# c99 hex digits (not the full format, just one I've seen)
0x[0-9a-fA-F](?:\.[0-9a-fA-F]*|)[pP]
-# Punycode
-\bxn--[-0-9a-z]+
# sha
sha\d+:[0-9]*[a-f]{3,}[0-9a-f]*
# sha-... -- uses a fancy capture
diff --git a/.github/actions/spelling/excludes.txt b/.github/actions/spelling/excludes.txt
index 3f3c4a691d5..441d1f295ca 100644
--- a/.github/actions/spelling/excludes.txt
+++ b/.github/actions/spelling/excludes.txt
@@ -106,6 +106,7 @@
^src/terminal/parser/ft_fuzzwrapper/run\.bat$
^src/terminal/parser/ut_parser/Base64Test.cpp$
^src/terminal/parser/ut_parser/run\.bat$
+^src/tools/benchcat
^src/tools/integrity/packageuwp/ConsoleUWP\.appxSources$
^src/tools/lnkd/lnkd\.bat$
^src/tools/pixels/pixels\.bat$
diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt
index fd999652844..626777f6bdb 100644
--- a/.github/actions/spelling/expect/expect.txt
+++ b/.github/actions/spelling/expect/expect.txt
@@ -1,4 +1,5 @@
aabbcc
+aarch
ABANDONFONT
abbcc
ABCDEFGHIJKLMNOPQRSTUVWXY
@@ -104,6 +105,7 @@ bcx
bcz
BEFOREPARENT
beginthread
+benchcat
bgfx
bgidx
Bgk
@@ -157,7 +159,6 @@ capslock
CARETBLINKINGENABLED
CARRIAGERETURN
cascadia
-castsi
catid
cazamor
CBash
@@ -216,11 +217,11 @@ cmder
CMDEXT
cmh
CMOUSEBUTTONS
-cmpeq
cmt
cmw
cmyk
CNL
+cnn
cnt
CNTRL
Codeflow
@@ -260,7 +261,6 @@ condrv
conechokey
conemu
configurability
-confusables
conhost
conime
conimeinfo
@@ -425,6 +425,7 @@ DECDHL
decdld
DECDMAC
DECDWL
+DECECM
DECEKBD
DECERA
DECFI
@@ -440,6 +441,7 @@ DECMSR
DECNKM
DECNRCM
DECOM
+decommit
DECPCTERM
DECPS
DECRARA
@@ -814,7 +816,6 @@ HIBYTE
hicon
HIDEWINDOW
hinst
-Hirots
HISTORYBUFS
HISTORYNODUP
HISTORYSIZE
@@ -831,6 +832,8 @@ HMK
hmod
hmodule
hmon
+homeglyphs
+homoglyph
HORZ
hostable
hostlib
@@ -985,7 +988,7 @@ KVM
langid
LANGUAGELIST
lasterror
-lastexitcode
+LASTEXITCODE
LAYOUTRTL
lbl
LBN
@@ -1021,7 +1024,6 @@ lnkd
lnkfile
LNM
LOADONCALL
-loadu
LOBYTE
localappdata
locsrc
@@ -1030,6 +1032,7 @@ LOGFONT
LOGFONTA
LOGFONTW
logissue
+losslessly
loword
lparam
LPCCH
@@ -1152,7 +1155,6 @@ MOUSEACTIVATE
MOUSEFIRST
MOUSEHWHEEL
MOUSEMOVE
-movemask
MOVESTART
msb
msctf
@@ -1288,7 +1290,6 @@ nullability
nullness
nullonfailure
nullopts
-NULs
numlock
numpad
NUMSCROLL
@@ -1778,7 +1779,6 @@ somefile
SOURCEBRANCH
sourced
spammy
-spand
SRCCODEPAGE
SRCCOPY
SRCINVERT
@@ -2065,6 +2065,7 @@ vcpkg
vcprintf
vcxitems
vec
+vectorize
vectorized
VERCTRL
VERTBAR
@@ -2305,7 +2306,6 @@ xunit
xutr
XVIRTUALSCREEN
XWalk
-xwwyzz
xxyyzz
yact
YCast
diff --git a/.github/actions/spelling/patterns/patterns.txt b/.github/actions/spelling/patterns/patterns.txt
index 5acf5e9bfa6..6e4a39485df 100644
--- a/.github/actions/spelling/patterns/patterns.txt
+++ b/.github/actions/spelling/patterns/patterns.txt
@@ -27,6 +27,12 @@ ROY\sG\.\sBIV
# Python stringprefix / binaryprefix
\b(?:B|BR|Br|F|FR|Fr|R|RB|RF|Rb|Rf|U|UR|Ur|b|bR|br|f|fR|fr|r|rB|rF|rb|rf|u|uR|ur)'
+# SSE intrinsics like "_mm_subs_epu16"
+\b_mm(?:|256|512)_\w+\b
+
+# ARM NEON intrinsics like "vsubq_u16"
+\bv\w+_[fsu](?:8|16|32|64)\b
+
# Automatically suggested patterns
# hit-count: 3831 file-count: 582
# IServiceProvider
diff --git a/.vscode/settings.json b/.vscode/settings.json
index bb2f304e54a..e7802ba358c 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -103,7 +103,9 @@
"files.exclude": {
"**/bin/**": true,
"**/obj/**": true,
- "**/packages/**": true,
"**/Generated Files/**": true
+ },
+ "search.exclude": {
+ "**/packages/**": true
}
}
diff --git a/OpenConsole.sln b/OpenConsole.sln
index 571824e7250..c329b756ca4 100644
--- a/OpenConsole.sln
+++ b/OpenConsole.sln
@@ -420,6 +420,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TerminalStress", "src\tools
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "RenderingTests", "src\tools\RenderingTests\RenderingTests.vcxproj", "{37C995E0-2349-4154-8E77-4A52C0C7F46D}"
EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "benchcat", "src\tools\benchcat\benchcat.vcxproj", "{2C836962-9543-4CE5-B834-D28E1F124B66}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
AuditMode|Any CPU = AuditMode|Any CPU
@@ -2780,11 +2782,8 @@ Global
{37C995E0-2349-4154-8E77-4A52C0C7F46D}.Debug|Any CPU.ActiveCfg = Debug|Win32
{37C995E0-2349-4154-8E77-4A52C0C7F46D}.Debug|ARM.ActiveCfg = Debug|Win32
{37C995E0-2349-4154-8E77-4A52C0C7F46D}.Debug|ARM64.ActiveCfg = Debug|ARM64
- {37C995E0-2349-4154-8E77-4A52C0C7F46D}.Debug|ARM64.Build.0 = Debug|ARM64
{37C995E0-2349-4154-8E77-4A52C0C7F46D}.Debug|x64.ActiveCfg = Debug|x64
- {37C995E0-2349-4154-8E77-4A52C0C7F46D}.Debug|x64.Build.0 = Debug|x64
{37C995E0-2349-4154-8E77-4A52C0C7F46D}.Debug|x86.ActiveCfg = Debug|Win32
- {37C995E0-2349-4154-8E77-4A52C0C7F46D}.Debug|x86.Build.0 = Debug|Win32
{37C995E0-2349-4154-8E77-4A52C0C7F46D}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32
{37C995E0-2349-4154-8E77-4A52C0C7F46D}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32
{37C995E0-2349-4154-8E77-4A52C0C7F46D}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64
@@ -2793,11 +2792,28 @@ Global
{37C995E0-2349-4154-8E77-4A52C0C7F46D}.Release|Any CPU.ActiveCfg = Release|Win32
{37C995E0-2349-4154-8E77-4A52C0C7F46D}.Release|ARM.ActiveCfg = Release|Win32
{37C995E0-2349-4154-8E77-4A52C0C7F46D}.Release|ARM64.ActiveCfg = Release|ARM64
- {37C995E0-2349-4154-8E77-4A52C0C7F46D}.Release|ARM64.Build.0 = Release|ARM64
{37C995E0-2349-4154-8E77-4A52C0C7F46D}.Release|x64.ActiveCfg = Release|x64
- {37C995E0-2349-4154-8E77-4A52C0C7F46D}.Release|x64.Build.0 = Release|x64
{37C995E0-2349-4154-8E77-4A52C0C7F46D}.Release|x86.ActiveCfg = Release|Win32
- {37C995E0-2349-4154-8E77-4A52C0C7F46D}.Release|x86.Build.0 = Release|Win32
+ {2C836962-9543-4CE5-B834-D28E1F124B66}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32
+ {2C836962-9543-4CE5-B834-D28E1F124B66}.AuditMode|ARM.ActiveCfg = AuditMode|Win32
+ {2C836962-9543-4CE5-B834-D28E1F124B66}.AuditMode|ARM64.ActiveCfg = Release|ARM64
+ {2C836962-9543-4CE5-B834-D28E1F124B66}.AuditMode|x64.ActiveCfg = Release|x64
+ {2C836962-9543-4CE5-B834-D28E1F124B66}.AuditMode|x86.ActiveCfg = Release|Win32
+ {2C836962-9543-4CE5-B834-D28E1F124B66}.Debug|Any CPU.ActiveCfg = Debug|Win32
+ {2C836962-9543-4CE5-B834-D28E1F124B66}.Debug|ARM.ActiveCfg = Debug|Win32
+ {2C836962-9543-4CE5-B834-D28E1F124B66}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {2C836962-9543-4CE5-B834-D28E1F124B66}.Debug|x64.ActiveCfg = Debug|x64
+ {2C836962-9543-4CE5-B834-D28E1F124B66}.Debug|x86.ActiveCfg = Debug|Win32
+ {2C836962-9543-4CE5-B834-D28E1F124B66}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32
+ {2C836962-9543-4CE5-B834-D28E1F124B66}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32
+ {2C836962-9543-4CE5-B834-D28E1F124B66}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64
+ {2C836962-9543-4CE5-B834-D28E1F124B66}.Fuzzing|x64.ActiveCfg = Fuzzing|x64
+ {2C836962-9543-4CE5-B834-D28E1F124B66}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32
+ {2C836962-9543-4CE5-B834-D28E1F124B66}.Release|Any CPU.ActiveCfg = Release|Win32
+ {2C836962-9543-4CE5-B834-D28E1F124B66}.Release|ARM.ActiveCfg = Release|Win32
+ {2C836962-9543-4CE5-B834-D28E1F124B66}.Release|ARM64.ActiveCfg = Release|ARM64
+ {2C836962-9543-4CE5-B834-D28E1F124B66}.Release|x64.ActiveCfg = Release|x64
+ {2C836962-9543-4CE5-B834-D28E1F124B66}.Release|x86.ActiveCfg = Release|Win32
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -2904,6 +2920,7 @@ Global
{3C67784E-1453-49C2-9660-483E2CC7F7AD} = {40BD8415-DD93-4200-8D82-498DDDC08CC8}
{613CCB57-5FA9-48EF-80D0-6B1E319E20C4} = {A10C4720-DCA4-4640-9749-67F4314F527C}
{37C995E0-2349-4154-8E77-4A52C0C7F46D} = {A10C4720-DCA4-4640-9749-67F4314F527C}
+ {2C836962-9543-4CE5-B834-D28E1F124B66} = {A10C4720-DCA4-4640-9749-67F4314F527C}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3140B1B7-C8EE-43D1-A772-D82A7061A271}
diff --git a/build/pipelines/release.yml b/build/pipelines/release.yml
index 834df750d9d..8429f0abe0f 100644
--- a/build/pipelines/release.yml
+++ b/build/pipelines/release.yml
@@ -245,10 +245,6 @@ jobs:
TargetFolder: $(Build.ArtifactStagingDirectory)/appx
OverWrite: true
flattenFolders: true
- - task: AzureArtifacts.manifest-generator-task.manifest-generator-task.ManifestGeneratorTask@0
- displayName: 'Generate SBOM manifest'
- inputs:
- BuildDropPath: '$(System.ArtifactsDirectory)/appx'
- pwsh: |-
$Package = (Get-ChildItem "$(Build.ArtifactStagingDirectory)/appx" -Recurse -Filter "Cascadia*.msix" | Select -First 1)
@@ -271,20 +267,29 @@ jobs:
& "$(MakeAppxPath)" pack /h SHA256 /o /p $PackageFilename /d "$(Build.SourcesDirectory)\UnpackedTerminalPackage"
displayName: Re-pack the new Terminal package after signing
- - task: PublishBuildArtifacts@1
- displayName: Publish Artifact (appx)
- inputs:
- PathtoPublish: $(Build.ArtifactStagingDirectory)/appx
- ArtifactName: appx-$(BuildPlatform)-$(BuildConfiguration)
-
- pwsh: |-
$XamlAppxPath = (Get-Item "src\cascadia\CascadiaPackage\AppPackages\*\Dependencies\$(BuildPlatform)\Microsoft.UI.Xaml*.appx").FullName
- & .\build\scripts\New-UnpackagedTerminalDistribution.ps1 -TerminalAppX $(WindowsTerminalPackagePath) -XamlAppX $XamlAppxPath -Destination "$(Build.ArtifactStagingDirectory)/unpackaged"
+ & .\build\scripts\New-UnpackagedTerminalDistribution.ps1 -TerminalAppX $(WindowsTerminalPackagePath) -XamlAppX $XamlAppxPath -Destination "$(Build.ArtifactStagingDirectory)/appx"
displayName: Build Unpackaged Distribution
- - publish: $(Build.ArtifactStagingDirectory)/unpackaged
- artifact: unpackaged-$(BuildPlatform)-$(BuildConfiguration)
- displayName: Publish Artifact (unpackaged)
+ - task: AzureArtifacts.manifest-generator-task.manifest-generator-task.ManifestGeneratorTask@0
+ displayName: 'Generate SBOM manifest (application)'
+ inputs:
+ BuildDropPath: '$(System.ArtifactsDirectory)/appx'
+
+ - task: DropValidatorTask@0
+ displayName: 'Validate application SBOM manifest'
+ inputs:
+ BuildDropPath: '$(System.ArtifactsDirectory)/appx'
+ OutputPath: 'output.json'
+ ValidateSignature: true
+ Verbosity: 'Verbose'
+
+ - task: PublishBuildArtifacts@1
+ displayName: Publish Artifact (Terminal app)
+ inputs:
+ PathtoPublish: $(Build.ArtifactStagingDirectory)/appx
+ ArtifactName: terminal-$(BuildPlatform)-$(BuildConfiguration)
- ${{ if eq(parameters.buildConPTY, true) }}:
- task: CopyFiles@2
@@ -355,10 +360,13 @@ jobs:
inputs:
disableOutputRedirect: true
- ${{ each platform in parameters.buildPlatforms }}:
- - task: DownloadBuildArtifacts@0
+ - task: DownloadBuildArtifacts@1
displayName: Download Artifacts ${{ platform }}
inputs:
- artifactName: appx-${{ platform }}-Release
+ # Make sure to download the entire artifact, because it includes the SPDX SBOM
+ artifactName: terminal-${{ platform }}-Release
+ # Downloading to the source directory should ensure that the later SBOM generator can see the earlier SBOMs.
+ downloadPath: '$(Build.SourcesDirectory)/appx-artifacts'
# Add 3000 to the major version component, but only for the bundle.
# This is to ensure that it is newer than "2022.xx.yy.zz" or whatever the original bundle versions were before
# we switched to uniform naming.
@@ -368,7 +376,7 @@ jobs:
$Components[0] = ([int]$Components[0] + $VersionEpoch)
$BundleVersion = $Components -Join "."
New-Item -Type Directory "$(System.ArtifactsDirectory)\bundle"
- .\build\scripts\Create-AppxBundle.ps1 -InputPath "$(System.ArtifactsDirectory)" -ProjectName CascadiaPackage -BundleVersion $BundleVersion -OutputPath "$(System.ArtifactsDirectory)\bundle\$(BundleStemName)_$(XES_APPXMANIFESTVERSION)_8wekyb3d8bbwe.msixbundle"
+ .\build\scripts\Create-AppxBundle.ps1 -InputPath "$(Build.SourcesDirectory)/appx-artifacts" -ProjectName CascadiaPackage -BundleVersion $BundleVersion -OutputPath "$(System.ArtifactsDirectory)\bundle\$(BundleStemName)_$(XES_APPXMANIFESTVERSION)_8wekyb3d8bbwe.msixbundle"
displayName: Create WindowsTerminal*.msixbundle
- task: EsrpCodeSigning@1
displayName: Submit *.msixbundle to ESRP for code signing
@@ -405,6 +413,20 @@ jobs:
}
]
+ - task: AzureArtifacts.manifest-generator-task.manifest-generator-task.ManifestGeneratorTask@0
+ displayName: 'Generate SBOM manifest (bundle)'
+ inputs:
+ BuildDropPath: '$(System.ArtifactsDirectory)/bundle'
+ BuildComponentPath: '$(Build.SourcesDirectory)/appx-artifacts'
+
+ - task: DropValidatorTask@0
+ displayName: 'Validate bundle SBOM manifest'
+ inputs:
+ BuildDropPath: '$(System.ArtifactsDirectory)/bundle'
+ OutputPath: 'output.json'
+ ValidateSignature: true
+ Verbosity: 'Verbose'
+
- task: PublishBuildArtifacts@1
displayName: 'Publish Artifact: appxbundle-signed'
inputs:
@@ -431,7 +453,7 @@ jobs:
inputs:
disableOutputRedirect: true
- ${{ each platform in parameters.buildPlatforms }}:
- - task: DownloadBuildArtifacts@0
+ - task: DownloadBuildArtifacts@1
displayName: Download ${{ platform }} ConPTY binaries
inputs:
artifactName: conpty-dll-${{ platform }}-$(BuildConfiguration)
@@ -522,7 +544,7 @@ jobs:
inputs:
disableOutputRedirect: true
- ${{ each platform in parameters.buildPlatforms }}:
- - task: DownloadBuildArtifacts@0
+ - task: DownloadBuildArtifacts@1
displayName: Download ${{ platform }} PublicTerminalCore
inputs:
artifactName: wpf-dll-${{ platform }}-$(BuildConfiguration)
@@ -621,12 +643,13 @@ jobs:
- template: .\templates\restore-nuget-steps.yml
- # Download the appx-PLATFORM-CONFIG-VERSION artifact for every platform/version combo
+ # Download the terminal-PLATFORM-CONFIG-VERSION artifact for every platform/version combo
- ${{ each platform in parameters.buildPlatforms }}:
- - task: DownloadBuildArtifacts@0
+ - task: DownloadBuildArtifacts@1
displayName: Download Symbols ${{ platform }}
inputs:
- artifactName: appx-${{ platform }}-Release
+ artifactName: terminal-${{ platform }}-Release
+ itemPattern: '**/*.appxsym'
# It seems easier to do this -- download every appxsym -- then enumerate all the PDBs in the build directory for the
# public symbol push. Otherwise, we would have to list all of the PDB files one by one.
@@ -683,7 +706,7 @@ jobs:
submodules: true
- task: PkgESSetupBuild@12
displayName: Package ES - Setup Build
- - task: DownloadBuildArtifacts@0
+ - task: DownloadBuildArtifacts@1
displayName: Download Build Artifacts
inputs:
artifactName: appxbundle-signed
diff --git a/build/rules/GenerateSxsManifestsFromWinmds.targets b/build/rules/GenerateSxsManifestsFromWinmds.targets
index 600f9980ce0..6e17cc50dcb 100644
--- a/build/rules/GenerateSxsManifestsFromWinmds.targets
+++ b/build/rules/GenerateSxsManifestsFromWinmds.targets
@@ -30,7 +30,7 @@
DependsOnTargets="_ConsoleMapWinmdsToManifestFiles">
-
+
diff --git a/build/scripts/Invoke-FormattingCheck.ps1 b/build/scripts/Invoke-FormattingCheck.ps1
index ef780b5f480..d1110943d1f 100644
--- a/build/scripts/Invoke-FormattingCheck.ps1
+++ b/build/scripts/Invoke-FormattingCheck.ps1
@@ -10,7 +10,7 @@ function Invoke-CheckBadCodeFormatting() {
# returns a non-zero exit code if there are any diffs in the tracked files in the repo
git diff-index --quiet HEAD --
- if ($lastExitCode -eq 1) {
+ if ($LASTEXITCODE -eq 1) {
# Write the list of files that need updating to the log
git diff-index --name-only HEAD
diff --git a/build/scripts/Run-Tests.ps1 b/build/scripts/Run-Tests.ps1
index ea69396569d..1e52a8757e2 100644
--- a/build/scripts/Run-Tests.ps1
+++ b/build/scripts/Run-Tests.ps1
@@ -1,27 +1,36 @@
[CmdLetBinding()]
Param(
- [Parameter(Mandatory=$true, Position=0)][string]$MatchPattern,
- [Parameter(Mandatory=$true, Position=1)][string]$Platform,
- [Parameter(Mandatory=$true, Position=2)][string]$Configuration,
- [Parameter(Mandatory=$false, Position=3)][string]$LogPath,
- [Parameter(Mandatory=$false)][string]$Root = ".\bin\$Platform\$Configuration"
+ [Parameter(Mandatory=$true, Position=0)]
+ [string]$MatchPattern,
+ [Parameter(Mandatory=$true, Position=1)]
+ [string]$Platform,
+ [Parameter(Mandatory=$true, Position=2)]
+ [string]$Configuration,
+ [Parameter(Mandatory=$false, Position=3)]
+ [string]$LogPath,
+ [Parameter(Mandatory=$false)]
+ [string]$Root = ".\bin\$Platform\$Configuration"
)
-$testdlls = Get-ChildItem -Path "$Root" -Recurse -Filter $MatchPattern
+# Find test DLLs based on the provided root, match pattern, and recursion
+$testDlls = Get-ChildItem -Path $Root -Recurse -Filter $MatchPattern
+$args = @()
-$args = @();
-
-if ($LogPath)
-{
- $args += '/enablewttlogging';
- $args += '/appendwttlogging';
- $args += "/logFile:$LogPath";
- Write-Host "Wtt Logging Enabled";
+# Check if the LogPath parameter is provided and enable WTT logging
+if ($LogPath) {
+ $args += '/enablewttlogging'
+ $args += '/appendwttlogging'
+ $args += "/logFile:$LogPath"
+ Write-Host "WTT Logging Enabled"
}
-&"$Root\te.exe" $args $testdlls.FullName
+# Invoke the te.exe executable with arguments and test DLLs
+& "$Root\te.exe" $args $testDlls.FullName
-if ($lastexitcode -Ne 0) { Exit $lastexitcode }
+# Check the exit code of the te.exe process and exit accordingly
+if ($LASTEXITCODE -ne 0) {
+ Exit $LASTEXITCODE
+}
Exit 0
diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json
index 1900b2e4fc7..41a671626b6 100644
--- a/doc/cascadia/profiles.schema.json
+++ b/doc/cascadia/profiles.schema.json
@@ -421,6 +421,7 @@
"scrollToMark",
"clearMark",
"clearAllMarks",
+ "searchWeb",
"experimental.colorSelection",
"unbound"
],
@@ -1709,6 +1710,29 @@
}
]
},
+ "SearchWebAction": {
+ "description": "Search the web for selected text",
+ "allOf": [
+ {
+ "$ref": "#/$defs/ShortcutAction"
+ },
+ {
+ "properties": {
+ "action": {
+ "type": "string",
+ "const": "searchWeb"
+ },
+ "queryUrl": {
+ "type": "string",
+ "description": "URL of the web page to launch, %s is replaced with the selected text"
+ }
+ }
+ }
+ ],
+ "required": [
+ "queryUrl"
+ ]
+ },
"AdjustOpacityAction": {
"description": "Changes the opacity of the active Terminal window. If `relative` is specified, then this action will increase/decrease relative to the current opacity.",
"allOf": [
@@ -1813,6 +1837,19 @@
"description": "True if the Terminal should use a Mica backdrop for the window. This will apply underneath all controls (including the terminal panes and the titlebar)",
"type": "boolean",
"default": false
+ },
+ "experimental.rainbowFrame": {
+ "description": "When enabled, the frame of the window will cycle through all the colors. Enabling this will override the `frame` and `unfocusedFrame` settings.",
+ "type": "boolean",
+ "default": false
+ },
+ "frame": {
+ "description": "The color of the window frame when the window is inactive. This only works on Windows 11",
+ "$ref": "#/$defs/ThemeColor"
+ },
+ "unfocusedFrame": {
+ "description": "The color of the window frame when the window is inactive. This only works on Windows 11",
+ "$ref": "#/$defs/ThemeColor"
}
}
},
@@ -1991,6 +2028,9 @@
{
"$ref": "#/$defs/ColorSelectionAction"
},
+ {
+ "$ref": "#/$defs/SearchWebAction"
+ },
{
"type": "null"
}
diff --git a/src/buffer/out/Row.cpp b/src/buffer/out/Row.cpp
index a25e3b13696..3ea02083387 100644
--- a/src/buffer/out/Row.cpp
+++ b/src/buffer/out/Row.cpp
@@ -9,6 +9,8 @@
#include "textBuffer.hpp"
#include "../../types/inc/GlyphWidth.hpp"
+extern "C" int __isa_available;
+
// The STL is missing a std::iota_n analogue for std::iota, so I made my own.
template
constexpr OutIt iota_n(OutIt dest, Diff count, T val)
@@ -82,10 +84,7 @@ ROW::ROW(wchar_t* charsBuffer, uint16_t* charOffsetsBuffer, uint16_t rowWidth, c
_attr{ rowWidth, fillAttribute },
_columnCount{ rowWidth }
{
- if (_chars.data())
- {
- _init();
- }
+ _init();
}
void ROW::SetWrapForced(const bool wrap) noexcept
@@ -118,17 +117,25 @@ LineRendition ROW::GetLineRendition() const noexcept
return _lineRendition;
}
+uint16_t ROW::GetLineWidth() const noexcept
+{
+ const auto scale = _lineRendition != LineRendition::SingleWidth ? 1 : 0;
+ return _columnCount >> scale;
+}
+
// Routine Description:
// - Sets all properties of the ROW to default values
// Arguments:
// - Attr - The default attribute (color) to fill
// Return Value:
// -
-void ROW::Reset(const TextAttribute& attr)
+void ROW::Reset(const TextAttribute& attr) noexcept
{
_charsHeap.reset();
_chars = { _charsBuffer, _columnCount };
- _attr = { _columnCount, attr };
+ // Constructing and then moving objects into place isn't free.
+ // Modifying the existing object is _much_ faster.
+ *_attr.runs().unsafe_shrink_to_size(1) = til::rle_pair{ attr, _columnCount };
_lineRendition = LineRendition::SingleWidth;
_wrapForced = false;
_doubleBytePadded = false;
@@ -137,8 +144,117 @@ void ROW::Reset(const TextAttribute& attr)
void ROW::_init() noexcept
{
- std::fill_n(_chars.begin(), _columnCount, UNICODE_SPACE);
+#pragma warning(push)
+#pragma warning(disable : 26462) // The value pointed to by '...' is assigned only once, mark it as a pointer to const (con.4).
+#pragma warning(disable : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1).
+#pragma warning(disable : 26490) // Don't use reinterpret_cast (type.1).
+
+ // Fills _charsBuffer with whitespace and correspondingly _charOffsets
+ // with successive numbers from 0 to _columnCount+1.
+#if defined(TIL_SSE_INTRINSICS)
+ alignas(__m256i) static constexpr uint16_t whitespaceData[]{ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20 };
+ alignas(__m256i) static constexpr uint16_t offsetsData[]{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 };
+ alignas(__m256i) static constexpr uint16_t increment16Data[]{ 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16 };
+ alignas(__m128i) static constexpr uint16_t increment8Data[]{ 8, 8, 8, 8, 8, 8, 8, 8 };
+
+ // The AVX loop operates on 32 bytes at a minimum. Since _charsBuffer/_charOffsets uses 2 byte large
+ // wchar_t/uint16_t respectively, this translates to 16-element writes, which equals a _columnCount of 15,
+ // because it doesn't include the past-the-end char-offset as described in the _charOffsets member comment.
+ if (__isa_available >= __ISA_AVAILABLE_AVX2 && _columnCount >= 15)
+ {
+ auto chars = _charsBuffer;
+ auto charOffsets = _charOffsets.data();
+
+ // The backing buffer for both chars and charOffsets is guaranteed to be 16-byte aligned,
+ // but AVX operations are 32-byte large. As such, when we write out the last chunk, we
+ // have to align it to the ends of the 2 buffers. This results in a potential overlap of
+ // 16 bytes between the last write in the main loop below and the final write afterwards.
+ //
+ // An example:
+ // If you have a terminal between 16 and 23 columns the buffer has a size of 48 bytes.
+ // The main loop below will iterate once, as it writes out bytes 0-31 and then exits.
+ // The final write afterwards cannot write bytes 32-63 because that would write
+ // out of bounds. Instead it writes bytes 16-47, overwriting 16 overlapping bytes.
+ // This is better than branching and switching to SSE2, because both things are slow.
+ //
+ // Since we want to exit the main loop with at least 1 write left to do as the final write,
+ // we need to subtract 1 alignment from the buffer length (= 16 bytes). Since _columnCount is
+ // in wchar_t's we subtract -8. The same applies to the ~7 here vs ~15. If you squint slightly
+ // you'll see how this is effectively the inverse of what CalculateCharsBufferStride does.
+ const auto tailColumnOffset = gsl::narrow_cast((_columnCount - 8u) & ~7);
+ const auto charsEndLoop = chars + tailColumnOffset;
+ const auto charOffsetsEndLoop = charOffsets + tailColumnOffset;
+
+ const auto whitespace = _mm256_load_si256(reinterpret_cast(&whitespaceData[0]));
+ auto offsetsLoop = _mm256_load_si256(reinterpret_cast(&offsetsData[0]));
+ const auto offsets = _mm256_add_epi16(offsetsLoop, _mm256_set1_epi16(tailColumnOffset));
+
+ if (chars < charsEndLoop)
+ {
+ const auto increment = _mm256_load_si256(reinterpret_cast(&increment16Data[0]));
+
+ do
+ {
+ _mm256_storeu_si256(reinterpret_cast<__m256i*>(chars), whitespace);
+ _mm256_storeu_si256(reinterpret_cast<__m256i*>(charOffsets), offsetsLoop);
+ offsetsLoop = _mm256_add_epi16(offsetsLoop, increment);
+ chars += 16;
+ charOffsets += 16;
+ } while (chars < charsEndLoop);
+ }
+
+ _mm256_storeu_si256(reinterpret_cast<__m256i*>(charsEndLoop), whitespace);
+ _mm256_storeu_si256(reinterpret_cast<__m256i*>(charOffsetsEndLoop), offsets);
+ }
+ else
+ {
+ auto chars = _charsBuffer;
+ auto charOffsets = _charOffsets.data();
+ const auto charsEnd = chars + _columnCount;
+
+ const auto whitespace = _mm_load_si128(reinterpret_cast(&whitespaceData[0]));
+ const auto increment = _mm_load_si128(reinterpret_cast(&increment8Data[0]));
+ auto offsets = _mm_load_si128(reinterpret_cast(&offsetsData[0]));
+
+ do
+ {
+ _mm_storeu_si128(reinterpret_cast<__m128i*>(chars), whitespace);
+ _mm_storeu_si128(reinterpret_cast<__m128i*>(charOffsets), offsets);
+ offsets = _mm_add_epi16(offsets, increment);
+ chars += 8;
+ charOffsets += 8;
+ // If _columnCount is something like 120, the actual backing buffer for charOffsets is 121 items large.
+ // --> The while loop uses <= to emit at least 1 more write.
+ } while (chars <= charsEnd);
+ }
+#elif defined(TIL_ARM_NEON_INTRINSICS)
+ alignas(uint16x8_t) static constexpr uint16_t offsetsData[]{ 0, 1, 2, 3, 4, 5, 6, 7 };
+
+ auto chars = _charsBuffer;
+ auto charOffsets = _charOffsets.data();
+ const auto charsEnd = chars + _columnCount;
+
+ const auto whitespace = vdupq_n_u16(L' ');
+ const auto increment = vdupq_n_u16(8);
+ auto offsets = vld1q_u16(&offsetsData[0]);
+
+ do
+ {
+ vst1q_u16(chars, whitespace);
+ vst1q_u16(charOffsets, offsets);
+ offsets = vaddq_u16(offsets, increment);
+ chars += 8;
+ charOffsets += 8;
+ // If _columnCount is something like 120, the actual backing buffer for charOffsets is 121 items large.
+ // --> The while loop uses <= to emit at least 1 more write.
+ } while (chars <= charsEnd);
+#else
+#error "Vectorizing this function improves overall performance by up to 40%. Don't remove this warning, just add the vectorized code."
+ std::fill_n(_charsBuffer, _columnCount, UNICODE_SPACE);
std::iota(_charOffsets.begin(), _charOffsets.end(), uint16_t{ 0 });
+#endif
+
+#pragma warning(push)
}
void ROW::TransferAttributes(const til::small_rle& attr, til::CoordType newWidth)
@@ -147,6 +263,15 @@ void ROW::TransferAttributes(const til::small_rle& a
_attr.resize_trailing_extent(gsl::narrow(newWidth));
}
+void ROW::CopyFrom(const ROW& source)
+{
+ RowCopyTextFromState state{ .source = source };
+ CopyTextFrom(state);
+ TransferAttributes(source.Attributes(), _columnCount);
+ _lineRendition = source._lineRendition;
+ _wrapForced = source._wrapForced;
+}
+
// Returns the previous possible cursor position, preceding the given column.
// Returns 0 if column is less than or equal to 0.
til::CoordType ROW::NavigateToPrevious(til::CoordType column) const noexcept
@@ -316,10 +441,9 @@ OutputCellIterator ROW::WriteCells(OutputCellIterator it, const til::CoordType c
return it;
}
-bool ROW::SetAttrToEnd(const til::CoordType columnBegin, const TextAttribute attr)
+void ROW::SetAttrToEnd(const til::CoordType columnBegin, const TextAttribute attr)
{
_attr.replace(_clampedColumnInclusive(columnBegin), _attr.size(), attr);
- return true;
}
void ROW::ReplaceAttributes(const til::CoordType beginIndex, const til::CoordType endIndex, const TextAttribute& newAttr)
@@ -445,43 +569,51 @@ catch (...)
charsConsumed = ch - chBeg;
}
-til::CoordType ROW::CopyRangeFrom(til::CoordType columnBegin, til::CoordType columnLimit, const ROW& other, til::CoordType& otherBegin, til::CoordType otherLimit)
+void ROW::CopyTextFrom(RowCopyTextFromState& state)
try
{
- const auto otherColBeg = other._clampedColumnInclusive(otherBegin);
- const auto otherColLimit = other._clampedColumnInclusive(otherLimit);
- std::span charOffsets;
+ auto& source = state.source;
+ const auto sourceColBeg = source._clampedColumnInclusive(state.sourceColumnBegin);
+ const auto sourceColLimit = source._clampedColumnInclusive(state.sourceColumnLimit);
+ std::span charOffsets;
std::wstring_view chars;
- if (otherColBeg < otherColLimit)
+ if (sourceColBeg < sourceColLimit)
{
- charOffsets = other._charOffsets.subspan(otherColBeg, static_cast(otherColLimit) - otherColBeg + 1);
+ charOffsets = source._charOffsets.subspan(sourceColBeg, static_cast(sourceColLimit) - sourceColBeg + 1);
const auto charsOffset = charOffsets.front() & CharOffsetsMask;
// We _are_ using span. But C++ decided that string_view and span aren't convertible.
// _chars is a std::span for performance and because it refers to raw, shared memory.
#pragma warning(suppress : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1).
- chars = { other._chars.data() + charsOffset, other._chars.size() - charsOffset };
+ chars = { source._chars.data() + charsOffset, source._chars.size() - charsOffset };
}
- WriteHelper h{ *this, columnBegin, columnLimit, chars };
- if (!h.IsValid())
- {
- return h.colBeg;
- }
- // Any valid charOffsets array is at least 2 elements long (the 1st element is the start offset and the 2nd
- // element is the length of the first glyph) and begins/ends with a non-trailer offset. We don't really
- // need to test for the end offset, since `WriteHelper::WriteWithOffsets` already takes care of that.
- if (charOffsets.size() < 2 || WI_IsFlagSet(charOffsets.front(), CharOffsetsTrailer))
+ WriteHelper h{ *this, state.columnBegin, state.columnLimit, chars };
+
+ if (!h.IsValid() ||
+ // If we were to copy text from ourselves, we'd overwrite
+ // our _charOffsets and break Finish() which reads from it.
+ this == &state.source ||
+ // Any valid charOffsets array is at least 2 elements long (the 1st element is the start offset and the 2nd
+ // element is the length of the first glyph) and begins/ends with a non-trailer offset. We don't really
+ // need to test for the end offset, since `WriteHelper::WriteWithOffsets` already takes care of that.
+ charOffsets.size() < 2 || WI_IsFlagSet(charOffsets.front(), CharOffsetsTrailer))
{
- assert(false);
- otherBegin = other.size();
- return h.colBeg;
+ state.columnEnd = h.colBeg;
+ state.columnBeginDirty = h.colBeg;
+ state.columnEndDirty = h.colBeg;
+ state.sourceColumnEnd = source._columnCount;
+ return;
}
- h.CopyRangeFrom(charOffsets);
+
+ h.CopyTextFrom(charOffsets);
h.Finish();
- otherBegin += h.colEnd - h.colBeg;
- return h.colEndDirty;
+ // state.columnEnd is computed identical to ROW::ReplaceText. Check it out for more information.
+ state.columnEnd = h.charsConsumed == chars.size() ? h.colEnd : h.colLimit;
+ state.columnBeginDirty = h.colBegDirty;
+ state.columnEndDirty = h.colEndDirty;
+ state.sourceColumnEnd = sourceColBeg + h.colEnd - h.colBeg;
}
catch (...)
{
@@ -489,7 +621,7 @@ catch (...)
throw;
}
-[[msvc::forceinline]] void ROW::WriteHelper::CopyRangeFrom(const std::span& charOffsets) noexcept
+[[msvc::forceinline]] void ROW::WriteHelper::CopyTextFrom(const std::span& charOffsets) noexcept
{
// Since our `charOffsets` input is already in columns (just like the `ROW::_charOffsets`),
// we can directly look up the end char-offset, but...
@@ -504,26 +636,36 @@ catch (...)
const auto baseOffset = til::at(charOffsets, 0);
const auto endOffset = til::at(charOffsets, colEndInput);
const auto inToOutOffset = gsl::narrow_cast(chBeg - baseOffset);
+#pragma warning(suppress : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1).
+ const auto dst = row._charOffsets.data() + colEnd;
- // Now with the `colEndInput` figured out, we can easily copy the `charOffsets` into the `_charOffsets`.
- // It's possible to use SIMD for this loop for extra perf gains. Something like this for SSE2 (~8x faster):
- // const auto in = _mm_loadu_si128(...);
- // const auto off = _mm_and_epi32(in, _mm_set1_epi16(CharOffsetsMask));
- // const auto trailer = _mm_and_epi32(in, _mm_set1_epi16(CharOffsetsTrailer));
- // const auto out = _mm_or_epi32(_mm_add_epi16(off, _mm_set1_epi16(inToOutOffset)), trailer);
- // _mm_store_si128(..., out);
- for (uint16_t i = 0; i < colEndInput; ++i, ++colEnd)
- {
- const auto ch = til::at(charOffsets, i);
- const auto off = ch & CharOffsetsMask;
- const auto trailer = ch & CharOffsetsTrailer;
- til::at(row._charOffsets, colEnd) = gsl::narrow_cast((off + inToOutOffset) | trailer);
- }
+ _copyOffsets(dst, charOffsets.data(), colEndInput, inToOutOffset);
+ colEnd += colEndInput;
colEndDirty = gsl::narrow_cast(colBeg + colEndDirtyInput);
charsConsumed = endOffset - baseOffset;
}
+#pragma warning(push)
+#pragma warning(disable : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1).
+[[msvc::forceinline]] void ROW::WriteHelper::_copyOffsets(uint16_t* __restrict dst, const uint16_t* __restrict src, uint16_t size, uint16_t offset) noexcept
+{
+ __assume(src != nullptr);
+ __assume(dst != nullptr);
+
+ // All tested compilers (including MSVC) will neatly unroll and vectorize
+ // this loop, which is why it's written in this particular way.
+ for (const auto end = src + size; src != end; ++src, ++dst)
+ {
+ const uint16_t ch = *src;
+ const uint16_t off = ch & CharOffsetsMask;
+ const uint16_t trailer = ch & CharOffsetsTrailer;
+ const uint16_t newOff = off + offset;
+ *dst = newOff | trailer;
+ }
+}
+#pragma warning(pop)
+
[[msvc::forceinline]] void ROW::WriteHelper::Finish()
{
colEndDirty = row._adjustForward(colEndDirty);
diff --git a/src/buffer/out/Row.hpp b/src/buffer/out/Row.hpp
index 826d848b6aa..3733a11ffc7 100644
--- a/src/buffer/out/Row.hpp
+++ b/src/buffer/out/Row.hpp
@@ -26,6 +26,7 @@ Revision History:
#include "OutputCell.hpp"
#include "OutputCellIterator.hpp"
+class ROW;
class TextBuffer;
enum class DelimiterClass
@@ -43,7 +44,7 @@ struct RowWriteState
// The column at which to start writing.
til::CoordType columnBegin = 0; // IN
// The first column which should not be written to anymore.
- til::CoordType columnLimit = 0; // IN
+ til::CoordType columnLimit = til::CoordTypeMax; // IN
// The column 1 past the last glyph that was successfully written into the row. If you need to call
// ReplaceAttributes() to colorize the written range, etc., this is the columnEnd parameter you want.
@@ -57,9 +58,56 @@ struct RowWriteState
til::CoordType columnEndDirty = 0; // OUT
};
+struct RowCopyTextFromState
+{
+ // The row to copy text from.
+ const ROW& source; // IN
+ // The column at which to start writing.
+ til::CoordType columnBegin = 0; // IN
+ // The first column which should not be written to anymore.
+ til::CoordType columnLimit = til::CoordTypeMax; // IN
+ // The column at which to start reading from source.
+ til::CoordType sourceColumnBegin = 0; // IN
+ // The first column which should not be read from anymore.
+ til::CoordType sourceColumnLimit = til::CoordTypeMax; // IN
+
+ til::CoordType columnEnd = 0; // OUT
+ // The first column that got modified by this write operation. In case that the first glyph we write overwrites
+ // the trailing half of a wide glyph, leadingSpaces will be 1 and this value will be 1 less than colBeg.
+ til::CoordType columnBeginDirty = 0; // OUT
+ // This is 1 past the last column that was modified and will be 1 past columnEnd if we overwrote
+ // the leading half of a wide glyph and had to fill the trailing half with whitespace.
+ til::CoordType columnEndDirty = 0; // OUT
+ // This is 1 past the last column that was read from.
+ til::CoordType sourceColumnEnd = 0; // OUT
+};
+
class ROW final
{
public:
+ // The implicit agreement between ROW and TextBuffer is that the `charsBuffer` and `charOffsetsBuffer`
+ // arrays have a minimum alignment of 16 Bytes and a size of `rowWidth+1`. The former is used to
+ // implement Reset() efficiently via SIMD and the latter is used to store the past-the-end offset
+ // into the `charsBuffer`. Even though the `charsBuffer` could be only `rowWidth` large we need them
+ // to be the same size so that the SIMD code can process both arrays in the same loop simultaneously.
+ // This wastes up to 5.8% memory but increases overall scrolling performance by around 40%.
+ // These methods exists to make this agreement explicit and serve as a reminder.
+ //
+ // TextBuffer calculates the distance in bytes between two ROWs (_bufferRowStride) as the sum of these values.
+ // As such it's important that we return sizes with a minimum alignment of alignof(ROW).
+ static constexpr size_t CalculateRowSize() noexcept
+ {
+ return (sizeof(ROW) + 15) & ~15;
+ }
+ static constexpr size_t CalculateCharsBufferSize(size_t columns) noexcept
+ {
+ return (columns * sizeof(wchar_t) + 16) & ~15;
+ }
+ static constexpr size_t CalculateCharOffsetsBufferSize(size_t columns) noexcept
+ {
+ return (columns * sizeof(uint16_t) + 16) & ~15;
+ }
+
ROW() = default;
ROW(wchar_t* charsBuffer, uint16_t* charOffsetsBuffer, uint16_t rowWidth, const TextAttribute& fillAttribute);
@@ -75,20 +123,22 @@ class ROW final
bool WasDoubleBytePadded() const noexcept;
void SetLineRendition(const LineRendition lineRendition) noexcept;
LineRendition GetLineRendition() const noexcept;
+ uint16_t GetLineWidth() const noexcept;
- void Reset(const TextAttribute& attr);
+ void Reset(const TextAttribute& attr) noexcept;
void TransferAttributes(const til::small_rle& attr, til::CoordType newWidth);
+ void CopyFrom(const ROW& source);
til::CoordType NavigateToPrevious(til::CoordType column) const noexcept;
til::CoordType NavigateToNext(til::CoordType column) const noexcept;
void ClearCell(til::CoordType column);
OutputCellIterator WriteCells(OutputCellIterator it, til::CoordType columnBegin, std::optional wrap = std::nullopt, std::optional limitRight = std::nullopt);
- bool SetAttrToEnd(til::CoordType columnBegin, TextAttribute attr);
+ void SetAttrToEnd(til::CoordType columnBegin, TextAttribute attr);
void ReplaceAttributes(til::CoordType beginIndex, til::CoordType endIndex, const TextAttribute& newAttr);
void ReplaceCharacters(til::CoordType columnBegin, til::CoordType width, const std::wstring_view& chars);
void ReplaceText(RowWriteState& state);
- til::CoordType CopyRangeFrom(til::CoordType columnBegin, til::CoordType columnLimit, const ROW& other, til::CoordType& otherBegin, til::CoordType otherLimit);
+ void CopyTextFrom(RowCopyTextFromState& state);
til::small_rle& Attributes() noexcept;
const til::small_rle& Attributes() const noexcept;
@@ -122,7 +172,8 @@ class ROW final
bool IsValid() const noexcept;
void ReplaceCharacters(til::CoordType width) noexcept;
void ReplaceText() noexcept;
- void CopyRangeFrom(const std::span& charOffsets) noexcept;
+ void CopyTextFrom(const std::span& charOffsets) noexcept;
+ static void _copyOffsets(uint16_t* dst, const uint16_t* src, uint16_t size, uint16_t offset) noexcept;
void Finish();
// Parent pointer.
diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp
index b3c2e590e37..f41a1442721 100644
--- a/src/buffer/out/textBuffer.cpp
+++ b/src/buffer/out/textBuffer.cpp
@@ -42,10 +42,187 @@ TextBuffer::TextBuffer(til::size screenBufferSize,
// Guard against resizing the text buffer to 0 columns/rows, which would break being able to insert text.
screenBufferSize.width = std::max(screenBufferSize.width, 1);
screenBufferSize.height = std::max(screenBufferSize.height, 1);
- _charBuffer = _allocateBuffer(screenBufferSize, _currentAttributes, _storage);
- _UpdateSize();
+ _reserve(screenBufferSize, defaultAttributes);
}
+TextBuffer::~TextBuffer()
+{
+ if (_buffer)
+ {
+ _destroy();
+ }
+}
+
+// I put these functions in a block at the start of the class, because they're the most
+// fundamental aspect of TextBuffer: It implements the basic gap buffer text storage.
+// It's also fairly tricky code.
+#pragma region buffer management
+#pragma warning(push)
+#pragma warning(disable : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1).
+#pragma warning(disable : 26490) // Don't use reinterpret_cast (type.1).
+
+// MEM_RESERVEs memory sufficient to store height-many ROW structs,
+// as well as their ROW::_chars and ROW::_charOffsets buffers.
+//
+// We use explicit virtual memory allocations to not taint the general purpose allocator
+// with our huge allocation, as well as to be able to reduce the private working set of
+// the application by only committing what we actually need. This reduces conhost's
+// memory usage from ~7MB down to just ~2MB at startup in the general case.
+void TextBuffer::_reserve(til::size screenBufferSize, const TextAttribute& defaultAttributes)
+{
+ const auto w = gsl::narrow(screenBufferSize.width);
+ const auto h = gsl::narrow(screenBufferSize.height);
+
+ constexpr auto rowSize = ROW::CalculateRowSize();
+ const auto charsBufferSize = ROW::CalculateCharsBufferSize(w);
+ const auto charOffsetsBufferSize = ROW::CalculateCharOffsetsBufferSize(w);
+ const auto rowStride = rowSize + charsBufferSize + charOffsetsBufferSize;
+ assert(rowStride % alignof(ROW) == 0);
+
+ // 65535*65535 cells would result in a allocSize of 8GiB.
+ // --> Use uint64_t so that we can safely do our calculations even on x86.
+ // We allocate 1 additional row, which will be used for GetScratchpadRow().
+ const auto rowCount = ::base::strict_cast(h) + 1;
+ const auto allocSize = gsl::narrow(rowCount * rowStride);
+
+ // NOTE: Modifications to this block of code might have to be mirrored over to ResizeTraditional().
+ // It constructs a temporary TextBuffer and then extracts the members below, overwriting itself.
+ _buffer = wil::unique_virtualalloc_ptr{
+ static_cast(THROW_LAST_ERROR_IF_NULL(VirtualAlloc(nullptr, allocSize, MEM_RESERVE, PAGE_READWRITE)))
+ };
+ _bufferEnd = _buffer.get() + allocSize;
+ _commitWatermark = _buffer.get();
+ _initialAttributes = defaultAttributes;
+ _bufferRowStride = rowStride;
+ _bufferOffsetChars = rowSize;
+ _bufferOffsetCharOffsets = rowSize + charsBufferSize;
+ _width = w;
+ _height = h;
+}
+
+// MEM_COMMITs the memory and constructs all ROWs up to and including the given row pointer.
+// It's expected that the caller verifies the parameter. It goes hand in hand with _getRowByOffsetDirect().
+//
+// Declaring this function as noinline allows _getRowByOffsetDirect() to be inlined,
+// which improves overall TextBuffer performance by ~6%. And all it cost is this annotation.
+// 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)
+{
+ const auto rowEnd = row + _bufferRowStride;
+ const auto remaining = gsl::narrow_cast(_bufferEnd - _commitWatermark);
+ const auto minimum = gsl::narrow_cast(rowEnd - _commitWatermark);
+ const auto ideal = minimum + _bufferRowStride * _commitReadAheadRowCount;
+ const auto size = std::min(remaining, ideal);
+
+ THROW_LAST_ERROR_IF_NULL(VirtualAlloc(_commitWatermark, size, MEM_COMMIT, PAGE_READWRITE));
+
+ _construct(_commitWatermark + size);
+}
+
+// Destructs and MEM_DECOMMITs all previously constructed ROWs.
+// You can use this (or rather the Reset() method) to fully clear the TextBuffer.
+void TextBuffer::_decommit() noexcept
+{
+ _destroy();
+ VirtualFree(_buffer.get(), 0, MEM_DECOMMIT);
+ _commitWatermark = _buffer.get();
+}
+
+// Constructs ROWs up to (excluding) the ROW pointed to by `until`.
+void TextBuffer::_construct(const std::byte* until) noexcept
+{
+ for (; _commitWatermark < until; _commitWatermark += _bufferRowStride)
+ {
+ const auto row = reinterpret_cast(_commitWatermark);
+ const auto chars = reinterpret_cast(_commitWatermark + _bufferOffsetChars);
+ const auto indices = reinterpret_cast(_commitWatermark + _bufferOffsetCharOffsets);
+ std::construct_at(row, chars, indices, _width, _initialAttributes);
+ }
+}
+
+// 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)
+ {
+ std::destroy_at(reinterpret_cast(it));
+ }
+}
+
+// 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;
+ THROW_HR_IF(E_UNEXPECTED, row < _buffer.get() || row >= _bufferEnd);
+
+ if (row >= _commitWatermark)
+ {
+ _commit(row);
+ }
+
+ return *reinterpret_cast(row);
+}
+
+// Returns the "user-visible" index of the last committed row, which can be used
+// to short-circuit some algorithms that try to scan the entire buffer.
+// Returns 0 if no rows are committed in.
+til::CoordType TextBuffer::_estimateOffsetOfLastCommittedRow() const noexcept
+{
+ const auto lastRowOffset = (_commitWatermark - _buffer.get()) / _bufferRowStride;
+ // This subtracts 2 from the offset to account for the:
+ // * scratchpad row at offset 0, whereas regular rows start at offset 1.
+ // * fact that _commitWatermark points _past_ the last committed row,
+ // but we want to return an index pointing at the last row.
+ return std::max(0, gsl::narrow_cast(lastRowOffset - 2));
+}
+
+// Retrieves a row from the buffer by its offset from the first row of the text buffer
+// (what corresponds to the top row of the screen buffer).
+const ROW& TextBuffer::GetRowByOffset(const til::CoordType index) const
+{
+ // The const_cast is safe because "const" never had any meaning in C++ in the first place.
+#pragma warning(suppress : 26492) // Don't use const_cast to cast away const or volatile (type.3).
+ return const_cast(this)->GetRowByOffset(index);
+}
+
+// Retrieves a row from the buffer by its offset from the first row of the text buffer
+// (what corresponds to the top row of the screen buffer).
+ROW& TextBuffer::GetRowByOffset(const til::CoordType index)
+{
+ // Rows are stored circularly, so the index you ask for is offset by the start position and mod the total of rows.
+ auto offset = (_firstRow + index) % _height;
+
+ // Support negative wrap around. This way an index of -1 will
+ // wrap to _rowCount-1 and make implementing scrolling easier.
+ if (offset < 0)
+ {
+ offset += _height;
+ }
+
+ // We add 1 to the row offset, because row "0" is the one returned by GetScratchpadRow().
+ return _getRowByOffsetDirect(gsl::narrow_cast(offset) + 1);
+}
+
+// Returns a row filled with whitespace and the current attributes, for you to freely use.
+ROW& TextBuffer::GetScratchpadRow()
+{
+ return GetScratchpadRow(_currentAttributes);
+}
+
+// Returns a row filled with whitespace and the given attributes, for you to freely use.
+ROW& TextBuffer::GetScratchpadRow(const TextAttribute& attributes)
+{
+ auto& r = _getRowByOffsetDirect(0);
+ r.Reset(attributes);
+ return r;
+}
+
+#pragma warning(pop)
+#pragma endregion
+
// Routine Description:
// - Copies properties from another text buffer into this one.
// - This is primarily to copy properties that would otherwise not be specified during CreateInstance
@@ -66,35 +243,7 @@ void TextBuffer::CopyProperties(const TextBuffer& OtherBuffer) noexcept
// - Total number of rows in the buffer
til::CoordType TextBuffer::TotalRowCount() const noexcept
{
- return gsl::narrow_cast(_storage.size());
-}
-
-// Routine Description:
-// - Retrieves a row from the buffer by its offset from the first row of the text buffer (what corresponds to
-// the top row of the screen buffer)
-// Arguments:
-// - Number of rows down from the first row of the buffer.
-// Return Value:
-// - const reference to the requested row. Asserts if out of bounds.
-const ROW& TextBuffer::GetRowByOffset(const til::CoordType index) const noexcept
-{
- // Rows are stored circularly, so the index you ask for is offset by the start position and mod the total of rows.
- const auto offsetIndex = gsl::narrow_cast(_firstRow + index) % _storage.size();
- return til::at(_storage, offsetIndex);
-}
-
-// Routine Description:
-// - Retrieves a row from the buffer by its offset from the first row of the text buffer (what corresponds to
-// the top row of the screen buffer)
-// Arguments:
-// - Number of rows down from the first row of the buffer.
-// Return Value:
-// - reference to the requested row. Asserts if out of bounds.
-ROW& TextBuffer::GetRowByOffset(const til::CoordType index) noexcept
-{
- // Rows are stored circularly, so the index you ask for is offset by the start position and mod the total of rows.
- const auto offsetIndex = gsl::narrow_cast(_firstRow + index) % _storage.size();
- return til::at(_storage, offsetIndex);
+ return _height;
}
// Routine Description:
@@ -270,9 +419,8 @@ bool TextBuffer::_AssertValidDoubleByteSequence(const DbcsAttribute dbcsAttribut
//Return Value:
// - true if we successfully prepared the buffer and moved the cursor
// - false otherwise (out of memory)
-bool TextBuffer::_PrepareForDoubleByteSequence(const DbcsAttribute dbcsAttribute)
+void TextBuffer::_PrepareForDoubleByteSequence(const DbcsAttribute dbcsAttribute)
{
- auto fSuccess = true;
// Now compensate if we don't have enough space for the upcoming double byte sequence
// We only need to compensate for leading bytes
if (dbcsAttribute == DbcsAttribute::Leading)
@@ -288,10 +436,9 @@ bool TextBuffer::_PrepareForDoubleByteSequence(const DbcsAttribute dbcsAttribute
row.SetDoubleBytePadded(true);
// then move the cursor forward and onto the next row
- fSuccess = IncrementCursor();
+ IncrementCursor();
}
}
- return fSuccess;
}
void TextBuffer::ConsumeGrapheme(std::wstring_view& chars) noexcept
@@ -301,23 +448,69 @@ void TextBuffer::ConsumeGrapheme(std::wstring_view& chars) noexcept
chars = til::utf16_pop(chars);
}
-// This function is intended for writing regular "lines" of text and only the `state.text` and`state.columnBegin`
-// fields are being used, whereas `state.columnLimit` is automatically overwritten by the line width of the given row.
-// This allows this function to automatically set the wrap-forced field of the row, which is also the return value.
-// The return value indicates to the caller whether the cursor should be moved to the next line.
-void TextBuffer::WriteLine(til::CoordType row, bool wrapAtEOL, const TextAttribute& attributes, RowWriteState& state)
+// This function is intended for writing regular "lines" of text as it'll set the wrap flag on the given row.
+// You can continue calling the function on the same row as long as state.columnEnd < state.columnLimit.
+void TextBuffer::Write(til::CoordType row, const TextAttribute& attributes, RowWriteState& state)
{
auto& r = GetRowByOffset(row);
-
r.ReplaceText(state);
r.ReplaceAttributes(state.columnBegin, state.columnEnd, attributes);
+ TriggerRedraw(Viewport::FromExclusive({ state.columnBeginDirty, row, state.columnEndDirty, row + 1 }));
+}
- if (state.columnEnd >= state.columnLimit)
+// Fills an area of the buffer with a given fill character(s) and attributes.
+void TextBuffer::FillRect(const til::rect& rect, const std::wstring_view& fill, const TextAttribute& attributes)
+{
+ if (!rect || fill.empty())
{
- r.SetWrapForced(wrapAtEOL);
+ return;
}
- TriggerRedraw(Viewport::FromExclusive({ state.columnBeginDirty, row, state.columnEndDirty, row + 1 }));
+ auto& scratchpad = GetScratchpadRow(attributes);
+
+ // The scratchpad row gets reset to whitespace by default, so there's no need to
+ // initialize it again. Filling with whitespace is the most common operation by far.
+ if (fill != L" ")
+ {
+ RowWriteState state{
+ .columnLimit = rect.right,
+ .columnEnd = rect.left,
+ };
+
+ // Fill the scratchpad row with consecutive copies of "fill" up to the amount we need.
+ //
+ // We don't just create a single string with N copies of "fill" and write that at once,
+ // because that might join neighboring combining marks unintentionally.
+ //
+ // Building the buffer this way is very wasteful and slow, but it's still 3x
+ // faster than what we had before and no one complained about that either.
+ // It's seldom used code and probably not worth optimizing for.
+ while (state.columnEnd < rect.right)
+ {
+ state.columnBegin = state.columnEnd;
+ state.text = fill;
+ scratchpad.ReplaceText(state);
+ }
+ }
+
+ // Fill the given rows with copies of the scratchpad row. That's a little
+ // slower when filling just a single row, but will be much faster for >1 rows.
+ {
+ RowCopyTextFromState state{
+ .source = scratchpad,
+ .columnBegin = rect.left,
+ .columnLimit = rect.right,
+ .sourceColumnBegin = rect.left,
+ };
+
+ for (auto y = rect.top; y < rect.bottom; ++y)
+ {
+ auto& r = GetRowByOffset(y);
+ r.CopyTextFrom(state);
+ r.ReplaceAttributes(rect.left, rect.right, attributes);
+ TriggerRedraw(Viewport::FromExclusive({ state.columnBeginDirty, y, state.columnEndDirty, y + 1 }));
+ }
+ }
}
// Routine Description:
@@ -413,53 +606,37 @@ OutputCellIterator TextBuffer::WriteLine(const OutputCellIterator givenIt,
//Return Value:
// - true if we successfully inserted the character
// - false otherwise (out of memory)
-bool TextBuffer::InsertCharacter(const std::wstring_view chars,
+void TextBuffer::InsertCharacter(const std::wstring_view chars,
const DbcsAttribute dbcsAttribute,
const TextAttribute attr)
{
// Ensure consistent buffer state for double byte characters based on the character type we're about to insert
- auto fSuccess = _PrepareForDoubleByteSequence(dbcsAttribute);
+ _PrepareForDoubleByteSequence(dbcsAttribute);
- if (fSuccess)
- {
- // Get the current cursor position
- const auto iRow = GetCursor().GetPosition().y; // row stored as logical position, not array position
- const auto iCol = GetCursor().GetPosition().x; // column logical and array positions are equal.
+ // Get the current cursor position
+ const auto iRow = GetCursor().GetPosition().y; // row stored as logical position, not array position
+ const auto iCol = GetCursor().GetPosition().x; // column logical and array positions are equal.
- // Get the row associated with the given logical position
- auto& Row = GetRowByOffset(iRow);
+ // Get the row associated with the given logical position
+ auto& Row = GetRowByOffset(iRow);
- // Store character and double byte data
- try
- {
- switch (dbcsAttribute)
- {
- case DbcsAttribute::Leading:
- Row.ReplaceCharacters(iCol, 2, chars);
- break;
- case DbcsAttribute::Trailing:
- Row.ReplaceCharacters(iCol - 1, 2, chars);
- break;
- default:
- Row.ReplaceCharacters(iCol, 1, chars);
- break;
- }
- }
- catch (...)
- {
- LOG_HR(wil::ResultFromCaughtException());
- return false;
- }
-
- // Store color data
- fSuccess = Row.SetAttrToEnd(iCol, attr);
- if (fSuccess)
- {
- // Advance the cursor
- fSuccess = IncrementCursor();
- }
+ // Store character and double byte data
+ switch (dbcsAttribute)
+ {
+ case DbcsAttribute::Leading:
+ Row.ReplaceCharacters(iCol, 2, chars);
+ break;
+ case DbcsAttribute::Trailing:
+ Row.ReplaceCharacters(iCol - 1, 2, chars);
+ break;
+ default:
+ Row.ReplaceCharacters(iCol, 1, chars);
+ break;
}
- return fSuccess;
+
+ // Store color data
+ Row.SetAttrToEnd(iCol, attr);
+ IncrementCursor();
}
//Routine Description:
@@ -471,9 +648,9 @@ bool TextBuffer::InsertCharacter(const std::wstring_view chars,
//Return Value:
// - true if we successfully inserted the character
// - false otherwise (out of memory)
-bool TextBuffer::InsertCharacter(const wchar_t wch, const DbcsAttribute dbcsAttribute, const TextAttribute attr)
+void TextBuffer::InsertCharacter(const wchar_t wch, const DbcsAttribute dbcsAttribute, const TextAttribute attr)
{
- return InsertCharacter({ &wch, 1 }, dbcsAttribute, attr);
+ InsertCharacter({ &wch, 1 }, dbcsAttribute, attr);
}
//Routine Description:
@@ -483,7 +660,7 @@ bool TextBuffer::InsertCharacter(const wchar_t wch, const DbcsAttribute dbcsAttr
// - - Always sets to wrap
//Return Value:
// -
-void TextBuffer::_SetWrapOnCurrentRow() noexcept
+void TextBuffer::_SetWrapOnCurrentRow()
{
_AdjustWrapOnCurrentRow(true);
}
@@ -495,7 +672,7 @@ void TextBuffer::_SetWrapOnCurrentRow() noexcept
// - fSet - True if this row has a wrap. False otherwise.
//Return Value:
// -
-void TextBuffer::_AdjustWrapOnCurrentRow(const bool fSet) noexcept
+void TextBuffer::_AdjustWrapOnCurrentRow(const bool fSet)
{
// The vertical position of the cursor represents the current row we're manipulating.
const auto uiCurrentRowOffset = GetCursor().GetPosition().y;
@@ -512,7 +689,7 @@ void TextBuffer::_AdjustWrapOnCurrentRow(const bool fSet) noexcept
//Return Value:
// - true if we successfully moved the cursor.
// - false otherwise (out of memory)
-bool TextBuffer::IncrementCursor()
+void TextBuffer::IncrementCursor()
{
// Cursor position is stored as logical array indices (starts at 0) for the window
// Buffer Size is specified as the "length" of the array. It would say 80 for valid values of 0-79.
@@ -522,7 +699,6 @@ bool TextBuffer::IncrementCursor()
// Move the cursor one position to the right
GetCursor().IncrementXPosition(1);
- auto fSuccess = true;
// If we've passed the final valid column...
if (GetCursor().GetPosition().x > iFinalColumnIndex)
{
@@ -530,9 +706,8 @@ bool TextBuffer::IncrementCursor()
_SetWrapOnCurrentRow();
// Then move the cursor to a new line
- fSuccess = NewlineCursor();
+ NewlineCursor();
}
- return fSuccess;
}
//Routine Description:
@@ -541,9 +716,8 @@ bool TextBuffer::IncrementCursor()
// -
//Return Value:
// - true if we successfully moved the cursor.
-bool TextBuffer::NewlineCursor()
+void TextBuffer::NewlineCursor()
{
- auto fSuccess = false;
const auto iFinalRowIndex = GetSize().BottomInclusive();
// Reset the cursor position to 0 and move down one line
@@ -557,22 +731,17 @@ bool TextBuffer::NewlineCursor()
GetCursor().SetYPosition(iFinalRowIndex);
// Instead increment the circular buffer to move us into the "oldest" row of the backing buffer
- fSuccess = IncrementCircularBuffer();
- }
- else
- {
- fSuccess = true;
+ IncrementCircularBuffer();
}
- return fSuccess;
}
//Routine Description:
// - Increments the circular buffer by one. Circular buffer is represented by FirstRow variable.
//Arguments:
-// - inVtMode - set to true in VT mode, so standard erase attributes are used for the new row.
+// - fillAttributes - the attributes with which the recycled row will be initialized.
//Return Value:
// - true if we successfully incremented the buffer.
-bool TextBuffer::IncrementCircularBuffer(const bool inVtMode)
+void TextBuffer::IncrementCircularBuffer(const TextAttribute& fillAttributes)
{
// FirstRow is at any given point in time the array index in the circular buffer that corresponds
// to the logical position 0 in the window (cursor coordinates and all other coordinates).
@@ -585,13 +754,6 @@ bool TextBuffer::IncrementCircularBuffer(const bool inVtMode)
_PruneHyperlinks();
// Second, clean out the old "first row" as it will become the "last row" of the buffer after the circle is performed.
- auto fillAttributes = _currentAttributes;
- if (inVtMode)
- {
- // The VT standard requires that the new row is initialized with
- // the current background color, but with no meta attributes set.
- fillAttributes.SetStandardErase();
- }
GetRowByOffset(0).Reset(fillAttributes);
{
// Now proceed to increment.
@@ -604,7 +766,6 @@ bool TextBuffer::IncrementCircularBuffer(const bool inVtMode)
_firstRow = 0;
}
}
- return true;
}
//Routine Description:
@@ -625,7 +786,7 @@ til::point TextBuffer::GetLastNonSpaceCharacter(std::optional TextBuffer::_allocateBuffer(til::size sz, const TextAttribute& attributes, std::vector& rows)
-{
- const auto w = gsl::narrow(sz.width);
- const auto h = gsl::narrow(sz.height);
-
- const auto charsBytes = w * sizeof(wchar_t);
- // The ROW::_indices array stores 1 more item than the buffer is wide.
- // That extra column stores the past-the-end _chars pointer.
- const auto indicesBytes = w * sizeof(uint16_t) + sizeof(uint16_t);
- const auto rowStride = charsBytes + indicesBytes;
- // 65535*65535 cells would result in a charsAreaSize of 8GiB.
- // --> Use uint64_t so that we can safely do our calculations even on x86.
- const auto allocSize = gsl::narrow(::base::strict_cast(rowStride) * ::base::strict_cast(h));
-
- auto buffer = wil::unique_virtualalloc_ptr{ static_cast(VirtualAlloc(nullptr, allocSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE)) };
- THROW_IF_NULL_ALLOC(buffer);
-
- auto data = std::span{ buffer.get(), allocSize }.begin();
-
- rows.resize(h);
- for (auto& row : rows)
- {
- const auto chars = til::bit_cast(&*data);
- const auto indices = til::bit_cast(&*(data + charsBytes));
- row = { chars, indices, w, attributes };
- data += rowStride;
- }
-
- return buffer;
-}
-
-void TextBuffer::_UpdateSize()
-{
- _size = Viewport::FromDimensions({ _storage.at(0).size(), gsl::narrow(_storage.size()) });
+ return Viewport::FromDimensions({ _width, _height });
}
void TextBuffer::_SetFirstRowIndex(const til::CoordType FirstRowIndex) noexcept
@@ -734,27 +859,21 @@ void TextBuffer::_SetFirstRowIndex(const til::CoordType FirstRowIndex) noexcept
_firstRow = FirstRowIndex;
}
-void TextBuffer::ScrollRows(const til::CoordType firstRow, const til::CoordType size, const til::CoordType delta)
+void TextBuffer::ScrollRows(const til::CoordType firstRow, til::CoordType size, const til::CoordType delta)
{
- // If we don't have to move anything, leave early.
if (delta == 0)
{
return;
}
- // OK. We're about to play games by moving rows around within the deque to
- // scroll a massive region in a faster way than copying things.
- // To make this easier, first correct the circular buffer to have the first row be 0 again.
- if (_firstRow != 0)
- {
- // Rotate the buffer to put the first row at the front.
- std::rotate(_storage.begin(), _storage.begin() + _firstRow, _storage.end());
+ // Since the for() loop uses !=, we must ensure that size is positive.
+ // A negative size doesn't make any sense anyways.
+ size = std::max(0, size);
- // The first row is now at the top.
- _firstRow = 0;
- }
+ til::CoordType y = 0;
+ til::CoordType end = 0;
+ til::CoordType step = 0;
- // Rotate just the subsection specified
if (delta < 0)
{
// The layout is like this:
@@ -764,33 +883,20 @@ void TextBuffer::ScrollRows(const til::CoordType firstRow, const til::CoordType
// | 0 begin
// | 1
// | 2
- // | 3 A. begin + firstRow + delta (because delta is negative)
+ // | 3 A. firstRow + delta (because delta is negative)
// | 4
- // | 5 B. begin + firstRow
+ // | 5 B. firstRow
// | 6
// | 7
- // | 8 C. begin + firstRow + size
+ // | 8 C. firstRow + size
// | 9
// | 10
// | 11
// - end
// We want B to slide up to A (the negative delta) and everything from [B,C) to slide up with it.
- // So the final layout will be
- // --- (storage) ----
- // | 0 begin
- // | 1
- // | 2
- // | 5
- // | 6
- // | 7
- // | 3
- // | 4
- // | 8
- // | 9
- // | 10
- // | 11
- // - end
- std::rotate(_storage.begin() + firstRow + delta, _storage.begin() + firstRow, _storage.begin() + firstRow + size);
+ y = firstRow;
+ end = firstRow + size;
+ step = 1;
}
else
{
@@ -803,31 +909,23 @@ void TextBuffer::ScrollRows(const til::CoordType firstRow, const til::CoordType
// | 2
// | 3
// | 4
- // | 5 A. begin + firstRow
+ // | 5 A. firstRow
// | 6
// | 7
- // | 8 B. begin + firstRow + size
+ // | 8 B. firstRow + size
// | 9
- // | 10 C. begin + firstRow + size + delta
+ // | 10 C. firstRow + size + delta
// | 11
// - end
// We want B-1 to slide down to C-1 (the positive delta) and everything from [A, B) to slide down with it.
- // So the final layout will be
- // --- (storage) ----
- // | 0 begin
- // | 1
- // | 2
- // | 3
- // | 4
- // | 8
- // | 9
- // | 5
- // | 6
- // | 7
- // | 10
- // | 11
- // - end
- std::rotate(_storage.begin() + firstRow, _storage.begin() + firstRow + size, _storage.begin() + firstRow + size + delta);
+ y = firstRow + size - 1;
+ end = firstRow - 1;
+ step = -1;
+ }
+
+ for (; y != end; y += step)
+ {
+ GetRowByOffset(y + delta).CopyFrom(GetRowByOffset(y));
}
}
@@ -841,7 +939,7 @@ const Cursor& TextBuffer::GetCursor() const noexcept
return _cursor;
}
-[[nodiscard]] TextAttribute TextBuffer::GetCurrentAttributes() const noexcept
+const TextAttribute& TextBuffer::GetCurrentAttributes() const noexcept
{
return _currentAttributes;
}
@@ -851,7 +949,12 @@ void TextBuffer::SetCurrentAttributes(const TextAttribute& currentAttributes) no
_currentAttributes = currentAttributes;
}
-void TextBuffer::SetCurrentLineRendition(const LineRendition lineRendition)
+void TextBuffer::SetWrapForced(const til::CoordType y, bool wrap)
+{
+ GetRowByOffset(y).SetWrapForced(wrap);
+}
+
+void TextBuffer::SetCurrentLineRendition(const LineRendition lineRendition, const TextAttribute& fillAttributes)
{
const auto cursorPosition = GetCursor().GetPosition();
const auto rowIndex = cursorPosition.y;
@@ -865,11 +968,9 @@ void TextBuffer::SetCurrentLineRendition(const LineRendition lineRendition)
if (lineRendition != LineRendition::SingleWidth)
{
const auto fillChar = L' ';
- auto fillAttrs = GetCurrentAttributes();
- fillAttrs.SetStandardErase();
const auto fillOffset = GetLineWidth(rowIndex);
const auto fillLength = gsl::narrow(GetSize().Width() - fillOffset);
- const OutputCellIterator fillData{ fillChar, fillAttrs, fillLength };
+ const OutputCellIterator fillData{ fillChar, fillAttributes, fillLength };
row.WriteCells(fillData, fillOffset, false);
// We also need to make sure the cursor is clamped within the new width.
GetCursor().SetPosition(ClampPositionWithinLine(cursorPosition));
@@ -878,7 +979,7 @@ void TextBuffer::SetCurrentLineRendition(const LineRendition lineRendition)
}
}
-void TextBuffer::ResetLineRenditionRange(const til::CoordType startRow, const til::CoordType endRow) noexcept
+void TextBuffer::ResetLineRenditionRange(const til::CoordType startRow, const til::CoordType endRow)
{
for (auto row = startRow; row < endRow; row++)
{
@@ -886,37 +987,37 @@ void TextBuffer::ResetLineRenditionRange(const til::CoordType startRow, const ti
}
}
-LineRendition TextBuffer::GetLineRendition(const til::CoordType row) const noexcept
+LineRendition TextBuffer::GetLineRendition(const til::CoordType row) const
{
return GetRowByOffset(row).GetLineRendition();
}
-bool TextBuffer::IsDoubleWidthLine(const til::CoordType row) const noexcept
+bool TextBuffer::IsDoubleWidthLine(const til::CoordType row) const
{
return GetLineRendition(row) != LineRendition::SingleWidth;
}
-til::CoordType TextBuffer::GetLineWidth(const til::CoordType row) const noexcept
+til::CoordType TextBuffer::GetLineWidth(const til::CoordType row) const
{
// Use shift right to quickly divide the width by 2 for double width lines.
const auto scale = IsDoubleWidthLine(row) ? 1 : 0;
return GetSize().Width() >> scale;
}
-til::point TextBuffer::ClampPositionWithinLine(const til::point position) const noexcept
+til::point TextBuffer::ClampPositionWithinLine(const til::point position) const
{
const auto rightmostColumn = GetLineWidth(position.y) - 1;
return { std::min(position.x, rightmostColumn), position.y };
}
-til::point TextBuffer::ScreenToBufferPosition(const til::point position) const noexcept
+til::point TextBuffer::ScreenToBufferPosition(const til::point position) const
{
// Use shift right to quickly divide the X pos by 2 for double width lines.
const auto scale = IsDoubleWidthLine(position.y) ? 1 : 0;
return { position.x >> scale, position.y };
}
-til::point TextBuffer::BufferToScreenPosition(const til::point position) const noexcept
+til::point TextBuffer::BufferToScreenPosition(const til::point position) const
{
// Use shift left to quickly multiply the X pos by 2 for double width lines.
const auto scale = IsDoubleWidthLine(position.y) ? 1 : 0;
@@ -926,14 +1027,10 @@ til::point TextBuffer::BufferToScreenPosition(const til::point position) const n
// Routine Description:
// - Resets the text contents of this buffer with the default character
// and the default current color attributes
-void TextBuffer::Reset()
+void TextBuffer::Reset() noexcept
{
- const auto attr = GetCurrentAttributes();
-
- for (auto& row : _storage)
- {
- row.Reset(attr);
- }
+ _decommit();
+ _initialAttributes = _currentAttributes;
}
// Routine Description:
@@ -950,55 +1047,34 @@ void TextBuffer::Reset()
try
{
- til::CoordType TopRow = 0; // new top row of the screen buffer
- if (newSize.height <= GetCursor().GetPosition().y)
+ TextBuffer newBuffer{ newSize, _currentAttributes, 0, false, _renderer };
+ const auto cursorRow = GetCursor().GetPosition().y;
+ const auto copyableRows = std::min(_height, newSize.height);
+ til::CoordType srcRow = 0;
+ til::CoordType dstRow = 0;
+
+ if (cursorRow >= newSize.height)
{
- TopRow = GetCursor().GetPosition().y - newSize.height + 1;
+ srcRow = cursorRow - newSize.height + 1;
}
- const auto TopRowIndex = gsl::narrow_cast(_firstRow + TopRow) % _storage.size();
-
- std::vector newStorage;
- auto newBuffer = _allocateBuffer(newSize, _currentAttributes, newStorage);
- // This basically imitates a std::rotate_copy(first, mid, last), but uses ROW::CopyRangeFrom() to do the copying.
+ for (; dstRow < copyableRows; ++dstRow, ++srcRow)
{
- const auto first = _storage.begin();
- const auto last = _storage.end();
- const auto mid = first + TopRowIndex;
- auto dest = newStorage.begin();
-
- std::span sourceRanges[]{
- { mid, last },
- { first, mid },
- };
-
- // Ensure we don't copy more from `_storage` than fit into `newStorage`.
- if (sourceRanges[0].size() > newStorage.size())
- {
- sourceRanges[0] = sourceRanges[0].subspan(0, newStorage.size());
- }
- if (const auto remaining = newStorage.size() - sourceRanges[0].size(); sourceRanges[1].size() > remaining)
- {
- sourceRanges[1] = sourceRanges[1].subspan(0, remaining);
- }
-
- for (const auto& sourceRange : sourceRanges)
- {
- for (const auto& oldRow : sourceRange)
- {
- til::CoordType begin = 0;
- dest->CopyRangeFrom(0, til::CoordTypeMax, oldRow, begin, til::CoordTypeMax);
- dest->TransferAttributes(oldRow.Attributes(), newSize.width);
- ++dest;
- }
- }
+ newBuffer.GetRowByOffset(dstRow).CopyFrom(GetRowByOffset(srcRow));
}
- _charBuffer = std::move(newBuffer);
- _storage = std::move(newStorage);
+ // NOTE: Keep this in sync with _reserve().
+ _buffer = std::move(newBuffer._buffer);
+ _bufferEnd = newBuffer._bufferEnd;
+ _commitWatermark = newBuffer._commitWatermark;
+ _initialAttributes = newBuffer._initialAttributes;
+ _bufferRowStride = newBuffer._bufferRowStride;
+ _bufferOffsetChars = newBuffer._bufferOffsetChars;
+ _bufferOffsetCharOffsets = newBuffer._bufferOffsetCharOffsets;
+ _width = newBuffer._width;
+ _height = newBuffer._height;
_SetFirstRowIndex(0);
- _UpdateSize();
}
CATCH_RETURN();
@@ -1068,17 +1144,6 @@ void TextBuffer::TriggerNewTextNotification(const std::wstring_view newText)
}
}
-// Routine Description:
-// - Retrieves the first row from the underlying buffer.
-// Arguments:
-// -
-// Return Value:
-// - reference to the first row.
-ROW& TextBuffer::_GetFirstRow() noexcept
-{
- return GetRowByOffset(0);
-}
-
// Method Description:
// - get delimiter class for buffer cell position
// - used for double click selection and uia word navigation
@@ -1087,7 +1152,7 @@ ROW& TextBuffer::_GetFirstRow() noexcept
// - wordDelimiters: the delimiters defined as a part of the DelimiterClass::DelimiterChar
// Return Value:
// - the delimiter class for the given char
-DelimiterClass TextBuffer::_GetDelimiterClassAt(const til::point pos, const std::wstring_view wordDelimiters) const noexcept
+DelimiterClass TextBuffer::_GetDelimiterClassAt(const til::point pos, const std::wstring_view wordDelimiters) const
{
return GetRowByOffset(pos.y).DelimiterClassAt(pos.x, wordDelimiters);
}
@@ -1153,7 +1218,7 @@ til::point TextBuffer::GetWordStart(const til::point target, const std::wstring_
// - wordDelimiters - what characters are we considering for the separation of words
// Return Value:
// - The til::point for the first character on the current/previous READABLE "word" (inclusive)
-til::point TextBuffer::_GetWordStartForAccessibility(const til::point target, const std::wstring_view wordDelimiters) const noexcept
+til::point TextBuffer::_GetWordStartForAccessibility(const til::point target, const std::wstring_view wordDelimiters) const
{
auto result = target;
const auto bufferSize = GetSize();
@@ -1198,7 +1263,7 @@ til::point TextBuffer::_GetWordStartForAccessibility(const til::point target, co
// - wordDelimiters - what characters are we considering for the separation of words
// Return Value:
// - The til::point for the first character on the current word or delimiter run (stopped by the left margin)
-til::point TextBuffer::_GetWordStartForSelection(const til::point target, const std::wstring_view wordDelimiters) const noexcept
+til::point TextBuffer::_GetWordStartForSelection(const til::point target, const std::wstring_view wordDelimiters) const
{
auto result = target;
const auto bufferSize = GetSize();
@@ -1319,7 +1384,7 @@ til::point TextBuffer::_GetWordEndForAccessibility(const til::point target, cons
// - wordDelimiters - what characters are we considering for the separation of words
// Return Value:
// - The til::point for the last character of the current word or delimiter run (stopped by right margin)
-til::point TextBuffer::_GetWordEndForSelection(const til::point target, const std::wstring_view wordDelimiters) const noexcept
+til::point TextBuffer::_GetWordEndForSelection(const til::point target, const std::wstring_view wordDelimiters) const
{
const auto bufferSize = GetSize();
@@ -2320,6 +2385,7 @@ HRESULT TextBuffer::Reflow(TextBuffer& oldBuffer,
TextBuffer& newBuffer,
const std::optional lastCharacterViewport,
std::optional> positionInfo)
+try
{
const auto& oldCursor = oldBuffer.GetCursor();
auto& newCursor = newBuffer.GetCursor();
@@ -2336,7 +2402,6 @@ HRESULT TextBuffer::Reflow(TextBuffer& oldBuffer,
auto fFoundCursorPos = false;
auto foundOldMutable = false;
auto foundOldVisible = false;
- auto hr = S_OK;
// Loop through all the rows of the old buffer and reprint them into the new buffer
til::CoordType iOldRow = 0;
for (; iOldRow < cOldRowsTotal; iOldRow++)
@@ -2392,20 +2457,12 @@ HRESULT TextBuffer::Reflow(TextBuffer& oldBuffer,
fFoundCursorPos = true;
}
- try
- {
- // TODO: MSFT: 19446208 - this should just use an iterator and the inserter...
- const auto glyph = row.GlyphAt(iOldCol);
- const auto dbcsAttr = row.DbcsAttrAt(iOldCol);
- const auto textAttr = row.GetAttrByColumn(iOldCol);
+ // TODO: MSFT: 19446208 - this should just use an iterator and the inserter...
+ const auto glyph = row.GlyphAt(iOldCol);
+ const auto dbcsAttr = row.DbcsAttrAt(iOldCol);
+ const auto textAttr = row.GetAttrByColumn(iOldCol);
- if (!newBuffer.InsertCharacter(glyph, dbcsAttr, textAttr))
- {
- hr = E_OUTOFMEMORY;
- break;
- }
- }
- CATCH_RETURN();
+ newBuffer.InsertCharacter(glyph, dbcsAttr, textAttr);
}
// GH#32: Copy the attributes from the rest of the row into this new buffer.
@@ -2438,16 +2495,9 @@ HRESULT TextBuffer::Reflow(TextBuffer& oldBuffer,
copyAttrCol < cOldColsTotal && newAttrColumn < newWidth;
copyAttrCol++, newAttrColumn++)
{
- try
- {
- // TODO: MSFT: 19446208 - this should just use an iterator and the inserter...
- const auto textAttr = row.GetAttrByColumn(copyAttrCol);
- if (!newRow.SetAttrToEnd(newAttrColumn, textAttr))
- {
- break;
- }
- }
- CATCH_LOG(); // Not worth dying over.
+ // TODO: MSFT: 19446208 - this should just use an iterator and the inserter...
+ const auto textAttr = row.GetAttrByColumn(copyAttrCol);
+ newRow.SetAttrToEnd(newAttrColumn, textAttr);
}
// If we found the old row that the caller was interested in, set the
@@ -2474,61 +2524,58 @@ HRESULT TextBuffer::Reflow(TextBuffer& oldBuffer,
}
}
- if (SUCCEEDED(hr))
+ // If we didn't have a full row to copy, insert a new
+ // line into the new buffer.
+ // Only do so if we were not forced to wrap. If we did
+ // force a word wrap, then the existing line break was
+ // only because we ran out of space.
+ if (iRight < cOldColsTotal && !row.WasWrapForced())
{
- // If we didn't have a full row to copy, insert a new
- // line into the new buffer.
- // Only do so if we were not forced to wrap. If we did
- // force a word wrap, then the existing line break was
- // only because we ran out of space.
- if (iRight < cOldColsTotal && !row.WasWrapForced())
+ if (!fFoundCursorPos && (iRight == cOldCursorPos.x && iOldRow == cOldCursorPos.y))
{
- if (!fFoundCursorPos && (iRight == cOldCursorPos.x && iOldRow == cOldCursorPos.y))
- {
- cNewCursorPos = newCursor.GetPosition();
- fFoundCursorPos = true;
- }
- // Only do this if it's not the final line in the buffer.
- // On the final line, we want the cursor to sit
- // where it is done printing for the cursor
- // adjustment to follow.
- if (iOldRow < cOldRowsTotal - 1)
- {
- hr = newBuffer.NewlineCursor() ? hr : E_OUTOFMEMORY;
- }
- else
+ cNewCursorPos = newCursor.GetPosition();
+ fFoundCursorPos = true;
+ }
+ // Only do this if it's not the final line in the buffer.
+ // On the final line, we want the cursor to sit
+ // where it is done printing for the cursor
+ // adjustment to follow.
+ if (iOldRow < cOldRowsTotal - 1)
+ {
+ newBuffer.NewlineCursor();
+ }
+ else
+ {
+ // If we are on the final line of the buffer, we have one more check.
+ // We got into this code path because we are at the right most column of a row in the old buffer
+ // that had a hard return (no wrap was forced).
+ // However, as we're inserting, the old row might have just barely fit into the new buffer and
+ // caused a new soft return (wrap was forced) putting the cursor at x=0 on the line just below.
+ // We need to preserve the memory of the hard return at this point by inserting one additional
+ // hard newline, otherwise we've lost that information.
+ // We only do this when the cursor has just barely poured over onto the next line so the hard return
+ // isn't covered by the soft one.
+ // e.g.
+ // The old line was:
+ // |aaaaaaaaaaaaaaaaaaa | with no wrap which means there was a newline after that final a.
+ // The cursor was here ^
+ // And the new line will be:
+ // |aaaaaaaaaaaaaaaaaaa| and show a wrap at the end
+ // | |
+ // ^ and the cursor is now there.
+ // If we leave it like this, we've lost the newline information.
+ // So we insert one more newline so a continued reflow of this buffer by resizing larger will
+ // continue to look as the original output intended with the newline data.
+ // After this fix, it looks like this:
+ // |aaaaaaaaaaaaaaaaaaa| no wrap at the end (preserved hard newline)
+ // | |
+ // ^ and the cursor is now here.
+ const auto coordNewCursor = newCursor.GetPosition();
+ if (coordNewCursor.x == 0 && coordNewCursor.y > 0)
{
- // If we are on the final line of the buffer, we have one more check.
- // We got into this code path because we are at the right most column of a row in the old buffer
- // that had a hard return (no wrap was forced).
- // However, as we're inserting, the old row might have just barely fit into the new buffer and
- // caused a new soft return (wrap was forced) putting the cursor at x=0 on the line just below.
- // We need to preserve the memory of the hard return at this point by inserting one additional
- // hard newline, otherwise we've lost that information.
- // We only do this when the cursor has just barely poured over onto the next line so the hard return
- // isn't covered by the soft one.
- // e.g.
- // The old line was:
- // |aaaaaaaaaaaaaaaaaaa | with no wrap which means there was a newline after that final a.
- // The cursor was here ^
- // And the new line will be:
- // |aaaaaaaaaaaaaaaaaaa| and show a wrap at the end
- // | |
- // ^ and the cursor is now there.
- // If we leave it like this, we've lost the newline information.
- // So we insert one more newline so a continued reflow of this buffer by resizing larger will
- // continue to look as the original output intended with the newline data.
- // After this fix, it looks like this:
- // |aaaaaaaaaaaaaaaaaaa| no wrap at the end (preserved hard newline)
- // | |
- // ^ and the cursor is now here.
- const auto coordNewCursor = newCursor.GetPosition();
- if (coordNewCursor.x == 0 && coordNewCursor.y > 0)
+ if (newBuffer.GetRowByOffset(coordNewCursor.y - 1).WasWrapForced())
{
- if (newBuffer.GetRowByOffset(coordNewCursor.y - 1).WasWrapForced())
- {
- hr = newBuffer.NewlineCursor() ? hr : E_OUTOFMEMORY;
- }
+ newBuffer.NewlineCursor();
}
}
}
@@ -2541,7 +2588,7 @@ HRESULT TextBuffer::Reflow(TextBuffer& oldBuffer,
// printable char gets reset. See GH #12567
auto newRowY = newCursor.GetPosition().y + 1;
const auto newHeight = newBuffer.GetSize().Height();
- const auto oldHeight = oldBuffer.GetSize().Height();
+ const auto oldHeight = oldBuffer._estimateOffsetOfLastCommittedRow() + 1;
for (;
iOldRow < oldHeight && newRowY < newHeight;
iOldRow++)
@@ -2561,77 +2608,61 @@ HRESULT TextBuffer::Reflow(TextBuffer& oldBuffer,
newRowY++;
}
- if (SUCCEEDED(hr))
+ // Finish copying remaining parameters from the old text buffer to the new one
+ newBuffer.CopyProperties(oldBuffer);
+ newBuffer.CopyHyperlinkMaps(oldBuffer);
+ newBuffer.CopyPatterns(oldBuffer);
+
+ // If we found where to put the cursor while placing characters into the buffer,
+ // just put the cursor there. Otherwise we have to advance manually.
+ if (fFoundCursorPos)
+ {
+ newCursor.SetPosition(cNewCursorPos);
+ }
+ else
{
- // Finish copying remaining parameters from the old text buffer to the new one
- newBuffer.CopyProperties(oldBuffer);
- newBuffer.CopyHyperlinkMaps(oldBuffer);
- newBuffer.CopyPatterns(oldBuffer);
+ // Advance the cursor to the same offset as before
+ // get the number of newlines and spaces between the old end of text and the old cursor,
+ // then advance that many newlines and chars
+ auto iNewlines = cOldCursorPos.y - cOldLastChar.y;
+ const auto iIncrements = cOldCursorPos.x - cOldLastChar.x;
+ const auto cNewLastChar = newBuffer.GetLastNonSpaceCharacter();
- // If we found where to put the cursor while placing characters into the buffer,
- // just put the cursor there. Otherwise we have to advance manually.
- if (fFoundCursorPos)
+ // If the last row of the new buffer wrapped, there's going to be one less newline needed,
+ // because the cursor is already on the next line
+ if (newBuffer.GetRowByOffset(cNewLastChar.y).WasWrapForced())
{
- newCursor.SetPosition(cNewCursorPos);
+ iNewlines = std::max(iNewlines - 1, 0);
}
else
{
- // Advance the cursor to the same offset as before
- // get the number of newlines and spaces between the old end of text and the old cursor,
- // then advance that many newlines and chars
- auto iNewlines = cOldCursorPos.y - cOldLastChar.y;
- const auto iIncrements = cOldCursorPos.x - cOldLastChar.x;
- const auto cNewLastChar = newBuffer.GetLastNonSpaceCharacter();
-
- // If the last row of the new buffer wrapped, there's going to be one less newline needed,
- // because the cursor is already on the next line
- if (newBuffer.GetRowByOffset(cNewLastChar.y).WasWrapForced())
+ // if this buffer didn't wrap, but the old one DID, then the d(columns) of the
+ // old buffer will be one more than in this buffer, so new need one LESS.
+ if (oldBuffer.GetRowByOffset(cOldLastChar.y).WasWrapForced())
{
iNewlines = std::max(iNewlines - 1, 0);
}
- else
- {
- // if this buffer didn't wrap, but the old one DID, then the d(columns) of the
- // old buffer will be one more than in this buffer, so new need one LESS.
- if (oldBuffer.GetRowByOffset(cOldLastChar.y).WasWrapForced())
- {
- iNewlines = std::max(iNewlines - 1, 0);
- }
- }
+ }
- for (auto r = 0; r < iNewlines; r++)
- {
- if (!newBuffer.NewlineCursor())
- {
- hr = E_OUTOFMEMORY;
- break;
- }
- }
- if (SUCCEEDED(hr))
- {
- for (auto c = 0; c < iIncrements - 1; c++)
- {
- if (!newBuffer.IncrementCursor())
- {
- hr = E_OUTOFMEMORY;
- break;
- }
- }
- }
+ for (auto r = 0; r < iNewlines; r++)
+ {
+ newBuffer.NewlineCursor();
+ }
+ for (auto c = 0; c < iIncrements - 1; c++)
+ {
+ newBuffer.IncrementCursor();
}
}
- if (SUCCEEDED(hr))
- {
- // Save old cursor size before we delete it
- const auto ulSize = oldCursor.GetSize();
+ // Save old cursor size before we delete it
+ const auto ulSize = oldCursor.GetSize();
- // Set size back to real size as it will be taking over the rendering duties.
- newCursor.SetSize(ulSize);
- }
+ // Set size back to real size as it will be taking over the rendering duties.
+ newCursor.SetSize(ulSize);
- return hr;
+ return S_OK;
}
+CATCH_RETURN()
// Method Description:
// - Adds or updates a hyperlink in our hyperlink table
diff --git a/src/buffer/out/textBuffer.hpp b/src/buffer/out/textBuffer.hpp
index 555531a25aa..5efeaaa741d 100644
--- a/src/buffer/out/textBuffer.hpp
+++ b/src/buffer/out/textBuffer.hpp
@@ -72,14 +72,22 @@ class TextBuffer final
const UINT cursorSize,
const bool isActiveBuffer,
Microsoft::Console::Render::Renderer& renderer);
- TextBuffer(const TextBuffer& a) = delete;
+
+ TextBuffer(const TextBuffer&) = delete;
+ TextBuffer(TextBuffer&&) = delete;
+ TextBuffer& operator=(const TextBuffer&) = delete;
+ TextBuffer& operator=(TextBuffer&&) = delete;
+
+ ~TextBuffer();
// Used for duplicating properties to another text buffer
void CopyProperties(const TextBuffer& OtherBuffer) noexcept;
// row manipulation
- const ROW& GetRowByOffset(const til::CoordType index) const noexcept;
- ROW& GetRowByOffset(const til::CoordType index) noexcept;
+ ROW& GetScratchpadRow();
+ ROW& GetScratchpadRow(const TextAttribute& attributes);
+ const ROW& GetRowByOffset(til::CoordType index) const;
+ ROW& GetRowByOffset(til::CoordType index);
TextBufferCellIterator GetCellDataAt(const til::point at) const;
TextBufferCellIterator GetCellLineDataAt(const til::point at) const;
@@ -90,7 +98,8 @@ class TextBuffer final
// Text insertion functions
static void ConsumeGrapheme(std::wstring_view& chars) noexcept;
- void WriteLine(til::CoordType row, bool wrapAtEOL, const TextAttribute& attributes, RowWriteState& state);
+ void Write(til::CoordType row, const TextAttribute& attributes, RowWriteState& state);
+ void FillRect(const til::rect& rect, const std::wstring_view& fill, const TextAttribute& attributes);
OutputCellIterator Write(const OutputCellIterator givenIt);
@@ -103,13 +112,13 @@ class TextBuffer final
const std::optional setWrap = std::nullopt,
const std::optional limitRight = std::nullopt);
- bool InsertCharacter(const wchar_t wch, const DbcsAttribute dbcsAttribute, const TextAttribute attr);
- bool InsertCharacter(const std::wstring_view chars, const DbcsAttribute dbcsAttribute, const TextAttribute attr);
- bool IncrementCursor();
- bool NewlineCursor();
+ void InsertCharacter(const wchar_t wch, const DbcsAttribute dbcsAttribute, const TextAttribute attr);
+ void InsertCharacter(const std::wstring_view chars, const DbcsAttribute dbcsAttribute, const TextAttribute attr);
+ void IncrementCursor();
+ void NewlineCursor();
// Scroll needs access to this to quickly rotate around the buffer.
- bool IncrementCircularBuffer(const bool inVtMode = false);
+ void IncrementCircularBuffer(const TextAttribute& fillAttributes = {});
til::point GetLastNonSpaceCharacter(std::optional viewOptional = std::nullopt) const;
@@ -124,21 +133,22 @@ class TextBuffer final
til::CoordType TotalRowCount() const noexcept;
- [[nodiscard]] TextAttribute GetCurrentAttributes() const noexcept;
+ const TextAttribute& GetCurrentAttributes() const noexcept;
void SetCurrentAttributes(const TextAttribute& currentAttributes) noexcept;
- void SetCurrentLineRendition(const LineRendition lineRendition);
- void ResetLineRenditionRange(const til::CoordType startRow, const til::CoordType endRow) noexcept;
- LineRendition GetLineRendition(const til::CoordType row) const noexcept;
- bool IsDoubleWidthLine(const til::CoordType row) const noexcept;
+ void SetWrapForced(til::CoordType y, bool wrap);
+ void SetCurrentLineRendition(const LineRendition lineRendition, const TextAttribute& fillAttributes);
+ void ResetLineRenditionRange(const til::CoordType startRow, const til::CoordType endRow);
+ LineRendition GetLineRendition(const til::CoordType row) const;
+ bool IsDoubleWidthLine(const til::CoordType row) const;
- til::CoordType GetLineWidth(const til::CoordType row) const noexcept;
- til::point ClampPositionWithinLine(const til::point position) const noexcept;
- til::point ScreenToBufferPosition(const til::point position) const noexcept;
- til::point BufferToScreenPosition(const til::point position) const noexcept;
+ til::CoordType GetLineWidth(const til::CoordType row) const;
+ til::point ClampPositionWithinLine(const til::point position) const;
+ til::point ScreenToBufferPosition(const til::point position) const;
+ til::point BufferToScreenPosition(const til::point position) const;
- void Reset();
+ void Reset() noexcept;
[[nodiscard]] HRESULT ResizeTraditional(const til::size newSize) noexcept;
@@ -219,23 +229,27 @@ class TextBuffer final
interval_tree::IntervalTree GetPatterns(const til::CoordType firstRow, const til::CoordType lastRow) const;
private:
- static wil::unique_virtualalloc_ptr _allocateBuffer(til::size sz, const TextAttribute& attributes, std::vector& rows);
+ void _reserve(til::size screenBufferSize, const TextAttribute& defaultAttributes);
+ void _commit(const std::byte* row);
+ void _decommit() noexcept;
+ void _construct(const std::byte* until) noexcept;
+ void _destroy() const noexcept;
+ ROW& _getRowByOffsetDirect(size_t offset);
+ til::CoordType _estimateOffsetOfLastCommittedRow() const noexcept;
- void _UpdateSize();
void _SetFirstRowIndex(const til::CoordType FirstRowIndex) noexcept;
- til::point _GetPreviousFromCursor() const noexcept;
- void _SetWrapOnCurrentRow() noexcept;
- void _AdjustWrapOnCurrentRow(const bool fSet) noexcept;
+ til::point _GetPreviousFromCursor() const;
+ void _SetWrapOnCurrentRow();
+ void _AdjustWrapOnCurrentRow(const bool fSet);
// Assist with maintaining proper buffer state for Double Byte character sequences
- bool _PrepareForDoubleByteSequence(const DbcsAttribute dbcsAttribute);
+ void _PrepareForDoubleByteSequence(const DbcsAttribute dbcsAttribute);
bool _AssertValidDoubleByteSequence(const DbcsAttribute dbcsAttribute);
- ROW& _GetFirstRow() noexcept;
void _ExpandTextRow(til::inclusive_rect& selectionRow) const;
- DelimiterClass _GetDelimiterClassAt(const til::point pos, const std::wstring_view wordDelimiters) const noexcept;
- til::point _GetWordStartForAccessibility(const til::point target, const std::wstring_view wordDelimiters) const noexcept;
- til::point _GetWordStartForSelection(const til::point target, const std::wstring_view wordDelimiters) const noexcept;
+ DelimiterClass _GetDelimiterClassAt(const til::point pos, const std::wstring_view wordDelimiters) const;
+ til::point _GetWordStartForAccessibility(const til::point target, const std::wstring_view wordDelimiters) const;
+ til::point _GetWordStartForSelection(const til::point target, const std::wstring_view wordDelimiters) const;
til::point _GetWordEndForAccessibility(const til::point target, const std::wstring_view wordDelimiters, const til::point limit) const;
- til::point _GetWordEndForSelection(const til::point target, const std::wstring_view wordDelimiters) const noexcept;
+ til::point _GetWordEndForSelection(const til::point target, const std::wstring_view wordDelimiters) const;
void _PruneHyperlinks();
static void _AppendRTFText(std::ostringstream& contentBuilder, const std::wstring_view& text);
@@ -249,13 +263,67 @@ class TextBuffer final
std::unordered_map _idsAndPatterns;
size_t _currentPatternId = 0;
- wil::unique_virtualalloc_ptr _charBuffer;
- std::vector _storage;
+ // This block describes the state of the underlying virtual memory buffer that holds all ROWs, text and attributes.
+ // Initially memory is only allocated with MEM_RESERVE to reduce the private working set of conhost.
+ // ROWs are laid out like this in memory:
+ // ROW <-- sizeof(ROW), stores
+ // (padding)
+ // ROW::_charsBuffer <-- _width * sizeof(wchar_t)
+ // (padding)
+ // ROW::_charOffsets <-- (_width + 1) * sizeof(uint16_t)
+ // (padding)
+ // ...
+ // Padding may exist for alignment purposes.
+ //
+ // The base (start) address of the memory arena.
+ wil::unique_virtualalloc_ptr _buffer;
+ // The past-the-end pointer of the memory arena.
+ std::byte* _bufferEnd = nullptr;
+ // The range between _buffer (inclusive) and _commitWatermark (exclusive) is the range of
+ // memory that has already been committed via MEM_COMMIT and contains ready-to-use ROWs.
+ //
+ // The problem is that calling VirtualAlloc(MEM_COMMIT) on each ROW one by one is extremely expensive, which forces
+ // us to commit ROWs in batches and avoid calling it on already committed ROWs. Let's say we commit memory in
+ // batches of 128 ROWs. One option to know whether a ROW has already been committed is to allocate a vector
+ // of size `(height + 127) / 128` and mark the corresponding slot as 1 if that 128-sized batch has been committed.
+ // That way we know not to commit it again. But ROWs aren't accessed randomly. Instead, they're usually accessed
+ // fairly linearly from row 1 to N. As such we can just commit ROWs up to the point of the highest accessed ROW
+ // plus some read-ahead of 128 ROWs. This is exactly what _commitWatermark stores: The highest accessed ROW plus
+ // some read-ahead. It's the amount of memory that has been committed and is ready to use.
+ //
+ // _commitWatermark will always be a multiple of _bufferRowStride away from _buffer.
+ // In other words, _commitWatermark itself will either point exactly onto the next ROW
+ // that should be committed or be equal to _bufferEnd when all ROWs are committed.
+ std::byte* _commitWatermark = nullptr;
+ // This will MEM_COMMIT 128 rows more than we need, to avoid us from having to call VirtualAlloc too often.
+ // This equates to roughly the following commit chunk sizes at these column counts:
+ // * 80 columns (the usual minimum) = 60KB chunks, 4.1MB buffer at 9001 rows
+ // * 120 columns (the most common) = 80KB chunks, 5.6MB buffer at 9001 rows
+ // * 400 columns (the usual maximum) = 220KB chunks, 15.5MB buffer at 9001 rows
+ // There's probably a better metric than this. (This comment was written when ROW had both,
+ // a _chars array containing text and a _charOffsets array contain column-to-text indices.)
+ static constexpr size_t _commitReadAheadRowCount = 128;
+ // Before TextBuffer was made to use virtual memory it initialized the entire memory arena with the initial
+ // attributes right away. To ensure it continues to work the way it used to, this stores these initial attributes.
+ TextAttribute _initialAttributes;
+ // ROW ---------------+--+--+
+ // (padding) | | v _bufferOffsetChars
+ // ROW::_charsBuffer | |
+ // (padding) | v _bufferOffsetCharOffsets
+ // ROW::_charOffsets |
+ // (padding) v _bufferRowStride
+ size_t _bufferRowStride = 0;
+ size_t _bufferOffsetChars = 0;
+ size_t _bufferOffsetCharOffsets = 0;
+ // The width of the buffer in columns.
+ uint16_t _width = 0;
+ // The height of the buffer in rows, excluding the scratchpad row.
+ uint16_t _height = 0;
+
TextAttribute _currentAttributes;
til::CoordType _firstRow = 0; // indexes top row (not necessarily 0)
Cursor _cursor;
- Microsoft::Console::Types::Viewport _size;
bool _isActiveBuffer = false;
diff --git a/src/buffer/out/textBufferCellIterator.cpp b/src/buffer/out/textBufferCellIterator.cpp
index f19b8a285bf..8a177dfec6e 100644
--- a/src/buffer/out/textBufferCellIterator.cpp
+++ b/src/buffer/out/textBufferCellIterator.cpp
@@ -287,7 +287,7 @@ ptrdiff_t TextBufferCellIterator::operator-(const TextBufferCellIterator& it)
// - Sets the coordinate position that this iterator will inspect within the text buffer on dereference.
// Arguments:
// - newPos - The new coordinate position.
-void TextBufferCellIterator::_SetPos(const til::point newPos) noexcept
+void TextBufferCellIterator::_SetPos(const til::point newPos)
{
if (newPos.y != _pos.y)
{
@@ -315,7 +315,7 @@ void TextBufferCellIterator::_SetPos(const til::point newPos) noexcept
// - pos - Position inside screen buffer bounds to retrieve row
// Return Value:
// - Pointer to the underlying CharRow structure
-const ROW* TextBufferCellIterator::s_GetRow(const TextBuffer& buffer, const til::point pos) noexcept
+const ROW* TextBufferCellIterator::s_GetRow(const TextBuffer& buffer, const til::point pos)
{
return &buffer.GetRowByOffset(pos.y);
}
diff --git a/src/buffer/out/textBufferCellIterator.hpp b/src/buffer/out/textBufferCellIterator.hpp
index 198bdac6ce1..30276f2e13c 100644
--- a/src/buffer/out/textBufferCellIterator.hpp
+++ b/src/buffer/out/textBufferCellIterator.hpp
@@ -49,9 +49,9 @@ class TextBufferCellIterator
til::point Pos() const noexcept;
protected:
- void _SetPos(const til::point newPos) noexcept;
+ void _SetPos(const til::point newPos);
void _GenerateView() noexcept;
- static const ROW* s_GetRow(const TextBuffer& buffer, const til::point pos) noexcept;
+ static const ROW* s_GetRow(const TextBuffer& buffer, const til::point pos);
til::small_rle::const_iterator _attrIter;
OutputCellView _view;
diff --git a/src/cascadia/PublicTerminalCore/HwndTerminalAutomationPeer.cpp b/src/cascadia/PublicTerminalCore/HwndTerminalAutomationPeer.cpp
index 41a1ee984d9..6eae42a665a 100644
--- a/src/cascadia/PublicTerminalCore/HwndTerminalAutomationPeer.cpp
+++ b/src/cascadia/PublicTerminalCore/HwndTerminalAutomationPeer.cpp
@@ -5,6 +5,7 @@
#include "HwndTerminalAutomationPeer.hpp"
#include "../../types/UiaTracing.h"
#include
+#include
#pragma warning(suppress : 4471) // We don't control UIAutomationClient
#include
@@ -13,6 +14,15 @@ using namespace Microsoft::Console::Types;
static constexpr wchar_t UNICODE_NEWLINE{ L'\n' };
+static int CheckDelayedProcException(int exception) noexcept
+{
+ if (exception == VcppException(ERROR_SEVERITY_ERROR, ERROR_PROC_NOT_FOUND))
+ {
+ return EXCEPTION_EXECUTE_HANDLER;
+ }
+ return EXCEPTION_CONTINUE_SEARCH;
+}
+
// Method Description:
// - creates a copy of the provided text with all of the control characters removed
// Arguments:
@@ -123,6 +133,12 @@ void HwndTerminalAutomationPeer::SignalCursorChanged()
void HwndTerminalAutomationPeer::NotifyNewOutput(std::wstring_view newOutput)
{
+ if (_notificationsUnavailable) [[unlikely]]
+ {
+ // What if you tried to notify, but God said no
+ return;
+ }
+
// Try to suppress any events (or event data)
// that is just the keypress the user made
auto sanitized{ Sanitize(newOutput) };
@@ -155,5 +171,19 @@ void HwndTerminalAutomationPeer::NotifyNewOutput(std::wstring_view newOutput)
const auto sanitizedBstr = wil::make_bstr_nothrow(sanitized.c_str());
static auto activityId = wil::make_bstr_nothrow(L"TerminalTextOutput");
- LOG_IF_FAILED(UiaRaiseNotificationEvent(this, NotificationKind_ActionCompleted, NotificationProcessing_All, sanitizedBstr.get(), activityId.get()));
+ _tryNotify(sanitizedBstr.get(), activityId.get());
+}
+
+// This needs to be a separate function because it is using SEH try/except, which
+// is incompatible with C++ unwinding
+void HwndTerminalAutomationPeer::_tryNotify(BSTR string, BSTR activity)
+{
+ __try
+ {
+ LOG_IF_FAILED(UiaRaiseNotificationEvent(this, NotificationKind_ActionCompleted, NotificationProcessing_All, string, activity));
+ }
+ __except (CheckDelayedProcException(GetExceptionCode()))
+ {
+ _notificationsUnavailable = true;
+ }
}
diff --git a/src/cascadia/PublicTerminalCore/HwndTerminalAutomationPeer.hpp b/src/cascadia/PublicTerminalCore/HwndTerminalAutomationPeer.hpp
index b9dab300cc1..4c6f6604ecc 100644
--- a/src/cascadia/PublicTerminalCore/HwndTerminalAutomationPeer.hpp
+++ b/src/cascadia/PublicTerminalCore/HwndTerminalAutomationPeer.hpp
@@ -38,5 +38,7 @@ class HwndTerminalAutomationPeer :
void NotifyNewOutput(std::wstring_view newOutput) override;
#pragma endregion
private:
+ void _tryNotify(BSTR string, BSTR activity);
std::deque _keyEvents;
+ bool _notificationsUnavailable{};
};
diff --git a/src/cascadia/PublicTerminalCore/PublicTerminalCore.vcxproj b/src/cascadia/PublicTerminalCore/PublicTerminalCore.vcxproj
index 9e091d49245..f8db236b21f 100644
--- a/src/cascadia/PublicTerminalCore/PublicTerminalCore.vcxproj
+++ b/src/cascadia/PublicTerminalCore/PublicTerminalCore.vcxproj
@@ -57,7 +57,8 @@
instead of APISet forwarders for easier Windows 7 compatibility. -->
- Uiautomationcore.lib;onecoreuap.lib;%(AdditionalDependencies)
+ delayimp.lib;Uiautomationcore.lib;onecoreuap.lib;%(AdditionalDependencies)
+ uiautomationcore.dll;%(DelayLoadDLLs)
diff --git a/src/cascadia/TerminalApp/App.base.h b/src/cascadia/TerminalApp/App.base.h
index 289ce39cabb..bece6324264 100644
--- a/src/cascadia/TerminalApp/App.base.h
+++ b/src/cascadia/TerminalApp/App.base.h
@@ -9,22 +9,35 @@ namespace winrt::TerminalApp::implementation
IXamlType GetXamlType(const ::winrt::Windows::UI::Xaml::Interop::TypeName& type)
{
- return _appProvider.GetXamlType(type);
+ return AppProvider()->GetXamlType(type);
}
IXamlType GetXamlType(const ::winrt::hstring& fullName)
{
- return _appProvider.GetXamlType(fullName);
+ return AppProvider()->GetXamlType(fullName);
}
::winrt::com_array<::winrt::Windows::UI::Xaml::Markup::XmlnsDefinition> GetXmlnsDefinitions()
{
- return _appProvider.GetXmlnsDefinitions();
+ return AppProvider()->GetXmlnsDefinitions();
+ }
+
+ void AddOtherProvider(const ::winrt::Windows::UI::Xaml::Markup::IXamlMetadataProvider& provider)
+ {
+ AppProvider()->AddOtherProvider(provider);
}
private:
bool _contentLoaded{ false };
- winrt::TerminalApp::XamlMetaDataProvider _appProvider;
+ winrt::com_ptr _appProvider;
+ winrt::com_ptr AppProvider()
+ {
+ if (!_appProvider)
+ {
+ _appProvider = winrt::make_self();
+ }
+ return _appProvider;
+ }
};
template
diff --git a/src/cascadia/TerminalApp/App.cpp b/src/cascadia/TerminalApp/App.cpp
index e90d0f649bc..017129ca5fc 100644
--- a/src/cascadia/TerminalApp/App.cpp
+++ b/src/cascadia/TerminalApp/App.cpp
@@ -29,6 +29,10 @@ namespace winrt::TerminalApp::implementation
void App::Initialize()
{
+ // LOAD BEARING
+ AddOtherProvider(winrt::Microsoft::Terminal::Control::XamlMetaDataProvider{});
+ AddOtherProvider(winrt::Microsoft::UI::Xaml::XamlTypeInfo::XamlControlsXamlMetaDataProvider{});
+
const auto dispatcherQueue = winrt::Windows::System::DispatcherQueue::GetForCurrentThread();
if (!dispatcherQueue)
{
@@ -102,4 +106,12 @@ namespace winrt::TerminalApp::implementation
// We used to support a pure UWP version of the Terminal. This method
// was only ever used to do UWP-specific setup of our App.
}
+
+ void App::PrepareForSettingsUI()
+ {
+ if (!std::exchange(_preparedForSettingsUI, true))
+ {
+ AddOtherProvider(winrt::Microsoft::Terminal::Settings::Editor::XamlMetaDataProvider{});
+ }
+ }
}
diff --git a/src/cascadia/TerminalApp/App.h b/src/cascadia/TerminalApp/App.h
index bb1e188aaed..a2c9b6a7c0c 100644
--- a/src/cascadia/TerminalApp/App.h
+++ b/src/cascadia/TerminalApp/App.h
@@ -19,6 +19,7 @@ namespace winrt::TerminalApp::implementation
TerminalApp::AppLogic Logic();
void Close();
+ void PrepareForSettingsUI();
bool IsDisposed() const
{
@@ -27,8 +28,8 @@ namespace winrt::TerminalApp::implementation
private:
winrt::Windows::UI::Xaml::Hosting::WindowsXamlManager _windowsXamlManager = nullptr;
- winrt::Windows::Foundation::Collections::IVector _providers = winrt::single_threaded_vector();
bool _bIsClosed = false;
+ bool _preparedForSettingsUI{ false };
};
}
diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp
index e5d05fa1942..ffe81054759 100644
--- a/src/cascadia/TerminalApp/AppActionHandlers.cpp
+++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp
@@ -1021,6 +1021,49 @@ namespace winrt::TerminalApp::implementation
args.Handled(true);
}
+ void TerminalPage::_HandleSearchForText(const IInspectable& /*sender*/,
+ const ActionEventArgs& args)
+ {
+ if (const auto termControl{ _GetActiveControl() })
+ {
+ if (termControl.HasSelection())
+ {
+ 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" ");
+
+ std::wstring queryUrl;
+ if (args)
+ {
+ if (const auto& realArgs = args.ActionArgs().try_as())
+ {
+ queryUrl = realArgs.QueryUrl().c_str();
+ }
+ }
+
+ // use global default if query URL is unspecified
+ if (queryUrl.empty())
+ {
+ queryUrl = _settings.GlobalSettings().SearchWebDefaultQueryUrl().c_str();
+ }
+
+ constexpr std::wstring_view queryToken{ L"%s" };
+ if (const auto pos{ queryUrl.find(queryToken) }; pos != std::wstring_view::npos)
+ {
+ queryUrl.replace(pos, queryToken.length(), Windows::Foundation::Uri::EscapeComponent(searchText));
+ }
+
+ winrt::Microsoft::Terminal::Control::OpenHyperlinkEventArgs shortcut{ queryUrl };
+ _OpenHyperlinkHandler(termControl, shortcut);
+ args.Handled(true);
+ }
+ }
+ }
+
void TerminalPage::_HandleGlobalSummon(const IInspectable& /*sender*/,
const ActionEventArgs& args)
{
diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw
index e33bc405dd8..1a20db37695 100644
--- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw
+++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw
@@ -214,6 +214,9 @@
Split Pane
+
+ Web Search
+
Color...
@@ -833,4 +836,4 @@
Moves tab to a new window
-
\ No newline at end of file
+
diff --git a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj
index 56e3367a97e..2b933ddfe91 100644
--- a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj
+++ b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj
@@ -16,6 +16,11 @@
-->
falsenested
+
+ DoNotGenerateOtherProviderstrue
diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp
index ddf05303a25..06944975df1 100644
--- a/src/cascadia/TerminalApp/TerminalPage.cpp
+++ b/src/cascadia/TerminalApp/TerminalPage.cpp
@@ -18,6 +18,7 @@
#include
#include "../../types/inc/utils.hpp"
+#include "App.h"
#include "ColorHelper.h"
#include "DebugTapConnection.h"
#include "SettingsTab.h"
@@ -3761,6 +3762,15 @@ namespace winrt::TerminalApp::implementation
// If we're holding the settings tab's switch command, don't create a new one, switch to the existing one.
if (!_settingsTab)
{
+ if (auto app{ winrt::Windows::UI::Xaml::Application::Current().try_as() })
+ {
+ if (auto appPrivate{ winrt::get_self(app) })
+ {
+ // Lazily load the Settings UI components so that we don't do it on startup.
+ appPrivate->PrepareForSettingsUI();
+ }
+ }
+
winrt::Microsoft::Terminal::Settings::Editor::MainPage sui{ _settings };
if (_hostingHwnd)
{
@@ -4281,26 +4291,29 @@ namespace winrt::TerminalApp::implementation
const TerminalSettingsCreateResult& controlSettings,
const Profile& profile)
{
- // Try to handle auto-elevation
- const auto requestedElevation = controlSettings.DefaultSettings().Elevate();
- const auto currentlyElevated = IsRunningElevated();
-
- // We aren't elevated, but we want to be.
- if (requestedElevation && !currentlyElevated)
+ // When duplicating a tab there aren't any newTerminalArgs.
+ if (!newTerminalArgs)
{
- // Manually set the Profile of the NewTerminalArgs to the guid we've
- // resolved to. If there was a profile in the NewTerminalArgs, this
- // will be that profile's GUID. If there wasn't, then we'll use
- // whatever the default profile's GUID is.
-
- newTerminalArgs.Profile(::Microsoft::Console::Utils::GuidToString(profile.Guid()));
+ return false;
+ }
- newTerminalArgs.StartingDirectory(_evaluatePathForCwd(controlSettings.DefaultSettings().StartingDirectory()));
+ const auto defaultSettings = controlSettings.DefaultSettings();
- _OpenElevatedWT(newTerminalArgs);
- return true;
+ // If we don't even want to elevate we can return early.
+ // If we're already elevated we can also return, because it doesn't get any more elevated than that.
+ if (!defaultSettings.Elevate() || IsRunningElevated())
+ {
+ return false;
}
- return false;
+
+ // Manually set the Profile of the NewTerminalArgs to the guid we've
+ // resolved to. If there was a profile in the NewTerminalArgs, this
+ // will be that profile's GUID. If there wasn't, then we'll use
+ // whatever the default profile's GUID is.
+ newTerminalArgs.Profile(::Microsoft::Console::Utils::GuidToString(profile.Guid()));
+ newTerminalArgs.StartingDirectory(_evaluatePathForCwd(defaultSettings.StartingDirectory()));
+ _OpenElevatedWT(newTerminalArgs);
+ return true;
}
// Method Description:
@@ -4473,6 +4486,16 @@ namespace winrt::TerminalApp::implementation
til::color bgColor = backgroundSolidBrush.Color();
+ Media::Brush terminalBrush{ nullptr };
+ if (const auto& control{ _GetActiveControl() })
+ {
+ terminalBrush = control.BackgroundBrush();
+ }
+ else if (const auto& settingsTab{ _GetFocusedTab().try_as() })
+ {
+ terminalBrush = settingsTab.Content().try_as().BackgroundBrush();
+ }
+
if (_settings.GlobalSettings().UseAcrylicInTabRow())
{
const auto acrylicBrush = Media::AcrylicBrush();
@@ -4487,18 +4510,6 @@ namespace winrt::TerminalApp::implementation
theme.TabRow().UnfocusedBackground()) :
ThemeColor{ nullptr } })
{
- const auto terminalBrush = [this]() -> Media::Brush {
- if (const auto& control{ _GetActiveControl() })
- {
- return control.BackgroundBrush();
- }
- else if (auto settingsTab = _GetFocusedTab().try_as())
- {
- return settingsTab.Content().try_as().BackgroundBrush();
- }
- return nullptr;
- }();
-
const auto themeBrush{ tabRowBg.Evaluate(res, terminalBrush, true) };
bgColor = ThemeColor::ColorFromBrush(themeBrush);
TitlebarBrush(themeBrush);
@@ -4528,11 +4539,27 @@ namespace winrt::TerminalApp::implementation
tabImpl->ThemeColor(tabBackground, tabUnfocusedBackground, bgColor);
}
}
-
// Update the new tab button to have better contrast with the new color.
// In theory, it would be convenient to also change these for the
// inactive tabs as well, but we're leaving that as a follow up.
_SetNewTabButtonColor(bgColor, bgColor);
+
+ // Third: the window frame. This is basically the same logic as the tab row background.
+ // We'll set our `FrameBrush` property, for the window to later use.
+ const auto windowTheme{ theme.Window() };
+ if (auto windowFrame{ windowTheme ? (_activated ? windowTheme.Frame() :
+ windowTheme.UnfocusedFrame()) :
+ ThemeColor{ nullptr } })
+ {
+ const auto themeBrush{ windowFrame.Evaluate(res, terminalBrush, true) };
+ FrameBrush(themeBrush);
+ }
+ else
+ {
+ // Nothing was set in the theme - fall back to null. The window will
+ // use that as an indication to use the default window frame.
+ FrameBrush(nullptr);
+ }
}
// Function Description:
@@ -4684,7 +4711,7 @@ namespace winrt::TerminalApp::implementation
}
void TerminalPage::_PopulateContextMenu(const IInspectable& sender,
- const bool /*withSelection*/)
+ const bool withSelection)
{
// withSelection can be used to add actions that only appear if there's
// selected text, like "search the web". In this initial draft, it's not
@@ -4741,6 +4768,11 @@ namespace winrt::TerminalApp::implementation
makeItem(RS_(L"PaneClose"), L"\xE89F", ActionAndArgs{ ShortcutAction::ClosePane, nullptr });
}
+ if (withSelection)
+ {
+ makeItem(RS_(L"SearchWebText"), L"\xF6FA", ActionAndArgs{ ShortcutAction::SearchForText, nullptr });
+ }
+
makeItem(RS_(L"TabClose"), L"\xE711", ActionAndArgs{ ShortcutAction::CloseTab, CloseTabArgs{ _GetFocusedTabIndex().value() } });
}
diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h
index 313a72b3dcf..7e424a5fc44 100644
--- a/src/cascadia/TerminalApp/TerminalPage.h
+++ b/src/cascadia/TerminalApp/TerminalPage.h
@@ -195,6 +195,7 @@ namespace winrt::TerminalApp::implementation
TYPED_EVENT(RequestReceiveContent, Windows::Foundation::IInspectable, winrt::TerminalApp::RequestReceiveContentArgs);
WINRT_OBSERVABLE_PROPERTY(winrt::Windows::UI::Xaml::Media::Brush, TitlebarBrush, _PropertyChangedHandlers, nullptr);
+ WINRT_OBSERVABLE_PROPERTY(winrt::Windows::UI::Xaml::Media::Brush, FrameBrush, _PropertyChangedHandlers, nullptr);
private:
friend struct TerminalPageT; // for Xaml to bind events
diff --git a/src/cascadia/TerminalApp/TerminalWindow.cpp b/src/cascadia/TerminalApp/TerminalWindow.cpp
index c95cf789315..7b60611db9b 100644
--- a/src/cascadia/TerminalApp/TerminalWindow.cpp
+++ b/src/cascadia/TerminalApp/TerminalWindow.cpp
@@ -944,12 +944,13 @@ namespace winrt::TerminalApp::implementation
winrt::Windows::UI::Xaml::Media::Brush TerminalWindow::TitlebarBrush()
{
- if (_root)
- {
- return _root->TitlebarBrush();
- }
- return { nullptr };
+ return _root ? _root->TitlebarBrush() : nullptr;
}
+ winrt::Windows::UI::Xaml::Media::Brush TerminalWindow::FrameBrush()
+ {
+ return _root ? _root->FrameBrush() : nullptr;
+ }
+
void TerminalWindow::WindowActivated(const bool activated)
{
if (_root)
diff --git a/src/cascadia/TerminalApp/TerminalWindow.h b/src/cascadia/TerminalApp/TerminalWindow.h
index a59c139a95c..a26b7c98bfb 100644
--- a/src/cascadia/TerminalApp/TerminalWindow.h
+++ b/src/cascadia/TerminalApp/TerminalWindow.h
@@ -128,6 +128,7 @@ namespace winrt::TerminalApp::implementation
winrt::TerminalApp::TaskbarState TaskbarState();
winrt::Windows::UI::Xaml::Media::Brush TitlebarBrush();
+ winrt::Windows::UI::Xaml::Media::Brush FrameBrush();
void WindowActivated(const bool activated);
bool GetMinimizeToNotificationArea();
diff --git a/src/cascadia/TerminalApp/TerminalWindow.idl b/src/cascadia/TerminalApp/TerminalWindow.idl
index d91de2dce88..2ca487be544 100644
--- a/src/cascadia/TerminalApp/TerminalWindow.idl
+++ b/src/cascadia/TerminalApp/TerminalWindow.idl
@@ -95,6 +95,7 @@ namespace TerminalApp
TaskbarState TaskbarState{ get; };
Windows.UI.Xaml.Media.Brush TitlebarBrush { get; };
+ Windows.UI.Xaml.Media.Brush FrameBrush { get; };
void WindowActivated(Boolean activated);
String GetWindowLayoutJson(Microsoft.Terminal.Settings.Model.LaunchPosition position);
diff --git a/src/cascadia/TerminalConnection/ConptyConnection.cpp b/src/cascadia/TerminalConnection/ConptyConnection.cpp
index 7bb1b399c7f..cecb5c7d759 100644
--- a/src/cascadia/TerminalConnection/ConptyConnection.cpp
+++ b/src/cascadia/TerminalConnection/ConptyConnection.cpp
@@ -325,7 +325,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
// handoff from an already-started PTY process.
if (!_inPipe)
{
- DWORD flags = PSEUDOCONSOLE_RESIZE_QUIRK | PSEUDOCONSOLE_WIN32_INPUT_MODE;
+ DWORD flags = PSEUDOCONSOLE_RESIZE_QUIRK;
// If we're using an existing buffer, we want the new connection
// to reuse the existing cursor. When not setting this flag, the
diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h
index 23aed68505f..8347523042f 100644
--- a/src/cascadia/TerminalControl/ControlCore.h
+++ b/src/cascadia/TerminalControl/ControlCore.h
@@ -25,8 +25,6 @@
#include "../buffer/out/search.h"
#include "../buffer/out/TextColor.h"
-#include
-
namespace ControlUnitTests
{
class ControlCoreTests;
@@ -151,6 +149,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation
int ViewHeight() const;
int BufferHeight() const;
+ bool HasSelection() const;
+ Windows::Foundation::Collections::IVector SelectedText(bool trimTrailingWhitespace) const;
+
bool BracketedPasteEnabled() const noexcept;
Windows::Foundation::Collections::IVector ScrollMarks() const;
@@ -194,9 +195,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
bool ShouldSendAlternateScroll(const unsigned int uiButton, const int32_t delta) const;
Core::Point CursorPosition() const;
- bool HasSelection() const;
bool CopyOnSelect() const;
- Windows::Foundation::Collections::IVector SelectedText(bool trimTrailingWhitespace) const;
Control::SelectionData SelectionInfo() const;
void SetSelectionAnchor(const til::point position);
void SetEndSelectionPoint(const til::point position);
diff --git a/src/cascadia/TerminalControl/ControlCore.idl b/src/cascadia/TerminalControl/ControlCore.idl
index 09081c5ab11..668f607808e 100644
--- a/src/cascadia/TerminalControl/ControlCore.idl
+++ b/src/cascadia/TerminalControl/ControlCore.idl
@@ -130,8 +130,6 @@ namespace Microsoft.Terminal.Control
void Search(String text, Boolean goForward, Boolean caseSensitive);
Microsoft.Terminal.Core.Color BackgroundColor { get; };
- Boolean HasSelection { get; };
- IVector SelectedText(Boolean trimTrailingWhitespace);
SelectionData SelectionInfo { get; };
SelectionInteractionMode SelectionMode();
diff --git a/src/cascadia/TerminalControl/EventArgs.h b/src/cascadia/TerminalControl/EventArgs.h
index b6902469c21..16f156ccbc3 100644
--- a/src/cascadia/TerminalControl/EventArgs.h
+++ b/src/cascadia/TerminalControl/EventArgs.h
@@ -194,3 +194,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation
WINRT_PROPERTY(uint32_t, ReplacementLength, 0);
};
}
+
+namespace winrt::Microsoft::Terminal::Control::factory_implementation
+{
+ BASIC_FACTORY(OpenHyperlinkEventArgs);
+}
diff --git a/src/cascadia/TerminalControl/EventArgs.idl b/src/cascadia/TerminalControl/EventArgs.idl
index 750504fa3da..f5db189aed5 100644
--- a/src/cascadia/TerminalControl/EventArgs.idl
+++ b/src/cascadia/TerminalControl/EventArgs.idl
@@ -40,6 +40,7 @@ namespace Microsoft.Terminal.Control
runtimeclass OpenHyperlinkEventArgs
{
+ OpenHyperlinkEventArgs(String uri);
String Uri { get; };
}
diff --git a/src/cascadia/TerminalControl/ICoreState.idl b/src/cascadia/TerminalControl/ICoreState.idl
index 405d1bd00f8..ed60ab4ea6c 100644
--- a/src/cascadia/TerminalControl/ICoreState.idl
+++ b/src/cascadia/TerminalControl/ICoreState.idl
@@ -45,6 +45,9 @@ namespace Microsoft.Terminal.Control
Int32 ViewHeight { get; };
Int32 BufferHeight { get; };
+ Boolean HasSelection { get; };
+ IVector SelectedText(Boolean trimTrailingWhitespace);
+
Boolean BracketedPasteEnabled { get; };
Microsoft.Terminal.TerminalConnection.ConnectionState ConnectionState { get; };
diff --git a/src/cascadia/TerminalControl/Resources/en-US/Resources.resw b/src/cascadia/TerminalControl/Resources/en-US/Resources.resw
index 4d95586e120..c94ab15d368 100644
--- a/src/cascadia/TerminalControl/Resources/en-US/Resources.resw
+++ b/src/cascadia/TerminalControl/Resources/en-US/Resources.resw
@@ -178,6 +178,10 @@
Ctrl+Click to follow link
+
+ Invalid URI
+ Whenever we encounter an invalid URI or URL we show this string as a warning.
+
Unable to find the selected font "{0}".
diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp
index b5e5a9832b6..fcead241bf5 100644
--- a/src/cascadia/TerminalControl/TermControl.cpp
+++ b/src/cascadia/TerminalControl/TermControl.cpp
@@ -3044,56 +3044,85 @@ namespace winrt::Microsoft::Terminal::Control::implementation
_core.ClearHoveredCell();
}
- winrt::fire_and_forget TermControl::_hoveredHyperlinkChanged(IInspectable /*sender*/,
- IInspectable /*args*/)
+ // Attackers abuse Unicode characters that happen to look similar to ASCII characters. Cyrillic for instance has
+ // its own glyphs for а, с, е, о, р, х, and у that look practically identical to their ASCII counterparts.
+ // This is called an "IDN homoglyph attack".
+ //
+ // But outright showing Punycode URIs only is similarly flawed as they can end up looking similar to valid ASCII URIs.
+ // xn--cnn.com for instance looks confusingly similar to cnn.com, but actually represents U+407E.
+ //
+ // An optimal solution would detect any URI that contains homoglyphs and show them in their Punycode form.
+ // Such a detector however is not quite trivial and requires constant maintenance, which this project's
+ // maintainers aren't currently well equipped to handle. As such we do the next best thing and show the
+ // Punycode encoding side-by-side with the Unicode string for any IDN.
+ static winrt::hstring sanitizeURI(winrt::hstring uri)
{
- auto weakThis{ get_weak() };
- co_await wil::resume_foreground(Dispatcher());
- if (auto self{ weakThis.get() })
+ if (uri.empty())
{
- auto lastHoveredCell = _core.HoveredCell();
- if (lastHoveredCell)
- {
- winrt::hstring uriText = _core.HoveredUriText();
- if (uriText.empty())
- {
- co_return;
- }
+ return uri;
+ }
- try
- {
- // DisplayUri will filter out non-printable characters and confusables.
- Windows::Foundation::Uri parsedUri{ uriText };
- if (!parsedUri)
- {
- co_return;
- }
- uriText = parsedUri.DisplayUri();
+ wchar_t punycodeBuffer[256];
+ wchar_t unicodeBuffer[256];
- const auto panel = SwapChainPanel();
- const auto scale = panel.CompositionScaleX();
- const auto offset = panel.ActualOffset();
+ // These functions return int, but are documented to only return positive numbers.
+ // Better make sure though. It allows us to pass punycodeLength right into IdnToUnicode.
+ const auto punycodeLength = std::max(0, IdnToAscii(0, uri.data(), gsl::narrow(uri.size()), &punycodeBuffer[0], 256));
+ const auto unicodeLength = std::max(0, IdnToUnicode(0, &punycodeBuffer[0], punycodeLength, &unicodeBuffer[0], 256));
- // Update the tooltip with the URI
- HoveredUri().Text(uriText);
+ if (punycodeLength <= 0 || unicodeLength <= 0)
+ {
+ return RS_(L"InvalidUri");
+ }
- // Set the border thickness so it covers the entire cell
- const auto charSizeInPixels = CharacterDimensions();
- const auto htInDips = charSizeInPixels.Height / scale;
- const auto wtInDips = charSizeInPixels.Width / scale;
- const Thickness newThickness{ wtInDips, htInDips, 0, 0 };
- HyperlinkTooltipBorder().BorderThickness(newThickness);
+ const std::wstring_view punycode{ &punycodeBuffer[0], gsl::narrow_cast(punycodeLength) };
+ const std::wstring_view unicode{ &unicodeBuffer[0], gsl::narrow_cast(unicodeLength) };
- // Compute the location of the top left corner of the cell in DIPS
- const til::point locationInDIPs{ _toPosInDips(lastHoveredCell.Value()) };
+ // IdnToAscii/IdnToUnicode return the input string as is if it's all
+ // plain ASCII. But we don't know if the input URI is Punycode or not.
+ // --> It's non-Punycode and ASCII if it round-trips.
+ if (uri == punycode && uri == unicode)
+ {
+ return uri;
+ }
- // Move the border to the top left corner of the cell
- OverlayCanvas().SetLeft(HyperlinkTooltipBorder(), locationInDIPs.x - offset.x);
- OverlayCanvas().SetTop(HyperlinkTooltipBorder(), locationInDIPs.y - offset.y);
- }
- CATCH_LOG();
- }
+ return winrt::hstring{ fmt::format(FMT_COMPILE(L"{}\n({})"), punycode, unicode) };
+ }
+
+ void TermControl::_hoveredHyperlinkChanged(const IInspectable& /*sender*/, const IInspectable& /*args*/)
+ {
+ const auto lastHoveredCell = _core.HoveredCell();
+ if (!lastHoveredCell)
+ {
+ return;
}
+
+ const auto uriText = sanitizeURI(_core.HoveredUriText());
+ if (uriText.empty())
+ {
+ return;
+ }
+
+ const auto panel = SwapChainPanel();
+ const auto scale = panel.CompositionScaleX();
+ const auto offset = panel.ActualOffset();
+
+ // Update the tooltip with the URI
+ HoveredUri().Text(uriText);
+
+ // Set the border thickness so it covers the entire cell
+ const auto charSizeInPixels = CharacterDimensions();
+ const auto htInDips = charSizeInPixels.Height / scale;
+ const auto wtInDips = charSizeInPixels.Width / scale;
+ const Thickness newThickness{ wtInDips, htInDips, 0, 0 };
+ HyperlinkTooltipBorder().BorderThickness(newThickness);
+
+ // Compute the location of the top left corner of the cell in DIPS
+ const til::point locationInDIPs{ _toPosInDips(lastHoveredCell.Value()) };
+
+ // Move the border to the top left corner of the cell
+ OverlayCanvas().SetLeft(HyperlinkTooltipBorder(), locationInDIPs.x - offset.x);
+ OverlayCanvas().SetTop(HyperlinkTooltipBorder(), locationInDIPs.y - offset.y);
}
winrt::fire_and_forget TermControl::_updateSelectionMarkers(IInspectable /*sender*/, Control::UpdateSelectionMarkersEventArgs args)
@@ -3312,6 +3341,15 @@ namespace winrt::Microsoft::Terminal::Control::implementation
return _core.Opacity();
}
+ bool TermControl::HasSelection() const
+ {
+ return _core.HasSelection();
+ }
+ Windows::Foundation::Collections::IVector TermControl::SelectedText(bool trimTrailingWhitespace) const
+ {
+ return _core.SelectedText(trimTrailingWhitespace);
+ }
+
// Method Description:
// - Called when the core raises a FoundMatch event. That's done in response
// to us starting a search query with ControlCore::Search.
diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h
index 33ea705d12c..9c5e5145146 100644
--- a/src/cascadia/TerminalControl/TermControl.h
+++ b/src/cascadia/TerminalControl/TermControl.h
@@ -73,6 +73,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation
int ViewHeight() const;
int BufferHeight() const;
+ bool HasSelection() const;
+ Windows::Foundation::Collections::IVector SelectedText(bool trimTrailingWhitespace) const;
+
bool BracketedPasteEnabled() const noexcept;
double BackgroundOpacity() const;
@@ -344,7 +347,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
void _CurrentCursorPositionHandler(const IInspectable& sender, const CursorPositionEventArgs& eventArgs);
void _FontInfoHandler(const IInspectable& sender, const FontInfoEventArgs& eventArgs);
- winrt::fire_and_forget _hoveredHyperlinkChanged(IInspectable sender, IInspectable args);
+ void _hoveredHyperlinkChanged(const IInspectable& sender, const IInspectable& args);
winrt::fire_and_forget _updateSelectionMarkers(IInspectable sender, Control::UpdateSelectionMarkersEventArgs args);
void _coreFontSizeChanged(const int fontWidth,
diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp
index 4b83e4f36f9..84aa9645201 100644
--- a/src/cascadia/TerminalCore/Terminal.cpp
+++ b/src/cascadia/TerminalCore/Terminal.cpp
@@ -21,35 +21,9 @@ using namespace Microsoft::Console::VirtualTerminal;
using PointTree = interval_tree::IntervalTree;
-static std::wstring _KeyEventsToText(std::deque>& inEventsToWrite)
-{
- std::wstring wstr = L"";
- for (const auto& ev : inEventsToWrite)
- {
- if (ev->EventType() == InputEventType::KeyEvent)
- {
- const auto& k = static_cast(*ev);
- const auto wch = k.GetCharData();
- wstr += wch;
- }
- }
- return wstr;
-}
-
#pragma warning(suppress : 26455) // default constructor is throwing, too much effort to rearrange at this time.
Terminal::Terminal()
{
- auto passAlongInput = [&](std::deque>& inEventsToWrite) {
- if (!_pfnWriteInput)
- {
- return;
- }
- const auto wstr = _KeyEventsToText(inEventsToWrite);
- _pfnWriteInput(wstr);
- };
-
- _terminalInput = std::make_unique(passAlongInput);
-
_renderSettings.SetColorAlias(ColorAlias::DefaultForeground, TextColor::DEFAULT_FOREGROUND, RGB(255, 255, 255));
_renderSettings.SetColorAlias(ColorAlias::DefaultBackground, TextColor::DEFAULT_BACKGROUND, RGB(0, 0, 0));
}
@@ -64,7 +38,7 @@ void Terminal::Create(til::size viewportSize, til::CoordType scrollbackLines, Re
const UINT cursorSize = 12;
_mainBuffer = std::make_unique(bufferSize, attr, cursorSize, true, renderer);
- auto dispatch = std::make_unique(*this, renderer, _renderSettings, *_terminalInput);
+ auto dispatch = std::make_unique(*this, renderer, _renderSettings, _terminalInput);
auto engine = std::make_unique(std::move(dispatch));
_stateMachine = std::make_unique(std::move(engine));
@@ -111,7 +85,7 @@ void Terminal::UpdateSettings(ICoreSettings settings)
_trimBlockSelection = settings.TrimBlockSelection();
_autoMarkPrompts = settings.AutoMarkPrompts();
- _terminalInput->ForceDisableWin32InputMode(settings.ForceVTInput());
+ _terminalInput.ForceDisableWin32InputMode(settings.ForceVTInput());
if (settings.TabColor() == nullptr)
{
@@ -540,7 +514,7 @@ void Terminal::TrySnapOnInput()
// - true, if we are tracking mouse input. False, otherwise
bool Terminal::IsTrackingMouseInput() const noexcept
{
- return _terminalInput->IsTrackingMouseInput();
+ return _terminalInput.IsTrackingMouseInput();
}
// Routine Description:
@@ -554,7 +528,7 @@ bool Terminal::IsTrackingMouseInput() const noexcept
bool Terminal::ShouldSendAlternateScroll(const unsigned int uiButton,
const int32_t delta) const noexcept
{
- return _terminalInput->ShouldSendAlternateScroll(uiButton, ::base::saturated_cast(delta));
+ return _terminalInput.ShouldSendAlternateScroll(uiButton, ::base::saturated_cast(delta));
}
// Method Description:
@@ -741,7 +715,7 @@ bool Terminal::SendKeyEvent(const WORD vkey,
}
const KeyEvent keyEv{ keyDown, 1, vkey, sc, ch, states.Value() };
- return _terminalInput->HandleKey(&keyEv);
+ return _handleTerminalInputResult(_terminalInput.HandleKey(&keyEv));
}
// Method Description:
@@ -765,7 +739,7 @@ bool Terminal::SendMouseEvent(til::point viewportPos, const unsigned int uiButto
// We shouldn't throw away perfectly good events when they're offscreen, so we just
// clamp them to be within the range [(0, 0), (W, H)].
_GetMutableViewport().ToOrigin().Clamp(viewportPos);
- return _terminalInput->HandleMouse(viewportPos, uiButton, GET_KEYSTATE_WPARAM(states.Value()), wheelDelta, state);
+ return _handleTerminalInputResult(_terminalInput.HandleMouse(viewportPos, uiButton, GET_KEYSTATE_WPARAM(states.Value()), wheelDelta, state));
}
// Method Description:
@@ -818,7 +792,7 @@ bool Terminal::SendCharEvent(const wchar_t ch, const WORD scanCode, const Contro
}
const KeyEvent keyDown{ true, 1, vkey, scanCode, ch, states.Value() };
- return _terminalInput->HandleKey(&keyDown);
+ return _handleTerminalInputResult(_terminalInput.HandleKey(&keyDown));
}
// Method Description:
@@ -829,9 +803,9 @@ bool Terminal::SendCharEvent(const wchar_t ch, const WORD scanCode, const Contro
// - focused: true if we're focused, false otherwise.
// Return Value:
// - none
-void Terminal::FocusChanged(const bool focused) noexcept
+void Terminal::FocusChanged(const bool focused)
{
- _terminalInput->HandleFocus(focused);
+ _handleTerminalInputResult(_terminalInput.HandleFocus(focused));
}
// Method Description:
@@ -955,6 +929,20 @@ catch (...)
return UNICODE_INVALID;
}
+[[maybe_unused]] bool Terminal::_handleTerminalInputResult(TerminalInput::OutputType&& out) const
+{
+ if (out)
+ {
+ const auto& str = *out;
+ if (_pfnWriteInput && !str.empty())
+ {
+ _pfnWriteInput(str);
+ }
+ return true;
+ }
+ return false;
+}
+
// Method Description:
// - It's possible for a single scan code on a keyboard to
// produce different key codes depending on the keyboard state.
diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp
index 76c09f6aef6..680a02bb5b4 100644
--- a/src/cascadia/TerminalCore/Terminal.hpp
+++ b/src/cascadia/TerminalCore/Terminal.hpp
@@ -126,7 +126,7 @@ class Microsoft::Terminal::Core::Terminal final :
void SetWorkingDirectory(std::wstring_view uri) override;
void PlayMidiNote(const int noteNumber, const int velocity, const std::chrono::microseconds duration) override;
void ShowWindow(bool showOrHide) override;
- void UseAlternateScreenBuffer() override;
+ void UseAlternateScreenBuffer(const TextAttribute& attrs) override;
void UseMainScreenBuffer() override;
void MarkPrompt(const Microsoft::Console::VirtualTerminal::DispatchTypes::ScrollMark& mark) override;
@@ -161,7 +161,7 @@ class Microsoft::Terminal::Core::Terminal final :
bool IsTrackingMouseInput() const noexcept;
bool ShouldSendAlternateScroll(const unsigned int uiButton, const int32_t delta) const noexcept;
- void FocusChanged(const bool focused) noexcept override;
+ void FocusChanged(const bool focused) override;
std::wstring GetHyperlinkAtViewportPosition(const til::point viewportPos);
std::wstring GetHyperlinkAtBufferPosition(const til::point bufferPos);
@@ -185,7 +185,7 @@ class Microsoft::Terminal::Core::Terminal final :
ULONG GetCursorHeight() const noexcept override;
ULONG GetCursorPixelWidth() const noexcept override;
CursorType GetCursorStyle() const noexcept override;
- bool IsCursorDoubleWidth() const noexcept override;
+ bool IsCursorDoubleWidth() const override;
const std::vector GetOverlays() const noexcept override;
const bool IsGridLineDrawingAllowed() noexcept override;
const std::wstring GetHyperlinkUri(uint16_t id) const override;
@@ -316,7 +316,7 @@ class Microsoft::Terminal::Core::Terminal final :
RenderSettings _renderSettings;
std::unique_ptr<::Microsoft::Console::VirtualTerminal::StateMachine> _stateMachine;
- std::unique_ptr<::Microsoft::Console::VirtualTerminal::TerminalInput> _terminalInput;
+ ::Microsoft::Console::VirtualTerminal::TerminalInput _terminalInput;
std::optional _title;
std::wstring _startingTitle;
@@ -415,6 +415,7 @@ class Microsoft::Terminal::Core::Terminal final :
static WORD _VirtualKeyFromCharacter(const wchar_t ch) noexcept;
static wchar_t _CharacterFromKeyEvent(const WORD vkey, const WORD scanCode, const ControlKeyStates states) noexcept;
+ [[maybe_unused]] bool _handleTerminalInputResult(::Microsoft::Console::VirtualTerminal::TerminalInput::OutputType&& out) const;
void _StoreKeyEvent(const WORD vkey, const WORD scanCode) noexcept;
WORD _TakeVirtualKeyFromLastKeyEvent(const WORD scanCode) noexcept;
diff --git a/src/cascadia/TerminalCore/TerminalApi.cpp b/src/cascadia/TerminalCore/TerminalApi.cpp
index fbc1699e974..cf7749cd83f 100644
--- a/src/cascadia/TerminalCore/TerminalApi.cpp
+++ b/src/cascadia/TerminalCore/TerminalApi.cpp
@@ -185,7 +185,7 @@ void Terminal::PlayMidiNote(const int noteNumber, const int velocity, const std:
_pfnPlayMidiNote(noteNumber, velocity, duration);
}
-void Terminal::UseAlternateScreenBuffer()
+void Terminal::UseAlternateScreenBuffer(const TextAttribute& attrs)
{
// the new alt buffer is exactly the size of the viewport.
_altBufferSize = _mutableViewport.Dimensions();
@@ -197,7 +197,7 @@ void Terminal::UseAlternateScreenBuffer()
// Create a new alt buffer
_altBuffer = std::make_unique(_altBufferSize,
- TextAttribute{},
+ attrs,
cursorSize,
true,
_mainBuffer->GetRenderer());
@@ -223,7 +223,7 @@ void Terminal::UseAlternateScreenBuffer()
// GH#3321: Make sure we let the TerminalInput know that we switched
// buffers. This might affect how we interpret certain mouse events.
- _terminalInput->UseAlternateScreenBuffer();
+ _terminalInput.UseAlternateScreenBuffer();
// Update scrollbars
_NotifyScrollEvent();
@@ -277,7 +277,7 @@ void Terminal::UseMainScreenBuffer()
// GH#3321: Make sure we let the TerminalInput know that we switched
// buffers. This might affect how we interpret certain mouse events.
- _terminalInput->UseMainScreenBuffer();
+ _terminalInput.UseMainScreenBuffer();
// Update scrollbars
_NotifyScrollEvent();
diff --git a/src/cascadia/TerminalCore/terminalrenderdata.cpp b/src/cascadia/TerminalCore/terminalrenderdata.cpp
index b4994777c71..69ea72d764f 100644
--- a/src/cascadia/TerminalCore/terminalrenderdata.cpp
+++ b/src/cascadia/TerminalCore/terminalrenderdata.cpp
@@ -70,7 +70,7 @@ CursorType Terminal::GetCursorStyle() const noexcept
return _activeBuffer().GetCursor().GetType();
}
-bool Terminal::IsCursorDoubleWidth() const noexcept
+bool Terminal::IsCursorDoubleWidth() const
{
const auto& buffer = _activeBuffer();
const auto position = buffer.GetCursor().GetPosition();
diff --git a/src/cascadia/TerminalSettingsEditor/Appearances.cpp b/src/cascadia/TerminalSettingsEditor/Appearances.cpp
index d1d15c2a244..8a3c9f943a2 100644
--- a/src/cascadia/TerminalSettingsEditor/Appearances.cpp
+++ b/src/cascadia/TerminalSettingsEditor/Appearances.cpp
@@ -20,6 +20,28 @@ using namespace winrt::Microsoft::Terminal::Settings::Model;
namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
+ bool Font::HasPowerlineCharacters()
+ {
+ if (!_hasPowerlineCharacters.has_value())
+ {
+ try
+ {
+ winrt::com_ptr font;
+ THROW_IF_FAILED(_family->GetFont(0, font.put()));
+ BOOL exists{};
+ // We're actually checking for the "Extended" PowerLine glyph set.
+ // They're more fun.
+ THROW_IF_FAILED(font->HasCharacter(0xE0B6, &exists));
+ _hasPowerlineCharacters = (exists == TRUE);
+ }
+ catch (...)
+ {
+ _hasPowerlineCharacters = false;
+ }
+ }
+ return _hasPowerlineCharacters.value_or(false);
+ }
+
AppearanceViewModel::AppearanceViewModel(const Model::AppearanceConfig& appearance) :
_appearance{ appearance }
{
@@ -288,25 +310,9 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
IInspectable Appearances::CurrentFontFace() const
{
- // look for the current font in our shown list of fonts
const auto& appearanceVM{ Appearance() };
const auto appearanceFontFace{ appearanceVM.FontFace() };
- const auto& currentFontList{ ShowAllFonts() ? ProfileViewModel::CompleteFontList() : ProfileViewModel::MonospaceFontList() };
- IInspectable fallbackFont;
- for (const auto& font : currentFontList)
- {
- if (font.LocalizedName() == appearanceFontFace)
- {
- return box_value(font);
- }
- else if (font.LocalizedName() == L"Cascadia Mono")
- {
- fallbackFont = box_value(font);
- }
- }
-
- // we couldn't find the desired font, set to "Cascadia Mono" since that ships by default
- return fallbackFont;
+ return box_value(ProfileViewModel::FindFontWithLocalizedName(appearanceFontFace));
}
void Appearances::FontFace_SelectionChanged(const IInspectable& /*sender*/, const SelectionChangedEventArgs& e)
diff --git a/src/cascadia/TerminalSettingsEditor/Appearances.h b/src/cascadia/TerminalSettingsEditor/Appearances.h
index 6793fa74be9..bc34e62978c 100644
--- a/src/cascadia/TerminalSettingsEditor/Appearances.h
+++ b/src/cascadia/TerminalSettingsEditor/Appearances.h
@@ -36,14 +36,22 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
struct Font : FontT
{
public:
- Font(std::wstring name, std::wstring localizedName) :
+ Font(std::wstring name, std::wstring localizedName, IDWriteFontFamily* family) :
_Name{ name },
- _LocalizedName{ localizedName } {};
+ _LocalizedName{ localizedName }
+ {
+ _family.copy_from(family);
+ }
hstring ToString() { return _LocalizedName; }
+ bool HasPowerlineCharacters();
WINRT_PROPERTY(hstring, Name);
WINRT_PROPERTY(hstring, LocalizedName);
+
+ private:
+ winrt::com_ptr _family;
+ std::optional _hasPowerlineCharacters;
};
struct AppearanceViewModel : AppearanceViewModelT, ViewModelHelper
diff --git a/src/cascadia/TerminalSettingsEditor/Appearances.idl b/src/cascadia/TerminalSettingsEditor/Appearances.idl
index 05c639b3b3b..55ff2be721d 100644
--- a/src/cascadia/TerminalSettingsEditor/Appearances.idl
+++ b/src/cascadia/TerminalSettingsEditor/Appearances.idl
@@ -19,6 +19,7 @@ namespace Microsoft.Terminal.Settings.Editor
{
String Name { get; };
String LocalizedName { get; };
+ Boolean HasPowerlineCharacters { get; };
}
runtimeclass AppearanceViewModel : Windows.UI.Xaml.Data.INotifyPropertyChanged
diff --git a/src/cascadia/TerminalSettingsEditor/ColorSchemeViewModel.cpp b/src/cascadia/TerminalSettingsEditor/ColorSchemeViewModel.cpp
index 30fc1018e82..4bd995b6c4a 100644
--- a/src/cascadia/TerminalSettingsEditor/ColorSchemeViewModel.cpp
+++ b/src/cascadia/TerminalSettingsEditor/ColorSchemeViewModel.cpp
@@ -50,6 +50,18 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
return _Name;
}
+ // This is used in the ComboBox and ListView.
+ // It's the only way to expose the name of the inner UI item so the ComboBox can do quick search
+ // and screen readers can read the item out loud.
+ winrt::hstring ColorSchemeViewModel::ToString()
+ {
+ if (IsDefaultScheme())
+ {
+ return hstring{ fmt::format(L"{0} ({1})", Name(), RS_(L"ColorScheme_DefaultTag/Text")) };
+ }
+ return Name();
+ }
+
bool ColorSchemeViewModel::IsDefaultScheme()
{
const auto defaultAppearance = _settings.ProfileDefaults().DefaultAppearance();
diff --git a/src/cascadia/TerminalSettingsEditor/ColorSchemeViewModel.h b/src/cascadia/TerminalSettingsEditor/ColorSchemeViewModel.h
index 6adb823ec85..539b0636c3c 100644
--- a/src/cascadia/TerminalSettingsEditor/ColorSchemeViewModel.h
+++ b/src/cascadia/TerminalSettingsEditor/ColorSchemeViewModel.h
@@ -25,10 +25,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
winrt::hstring Name();
void Name(winrt::hstring newName);
- hstring ToString()
- {
- return Name();
- }
+ hstring ToString();
bool RequestRename(winrt::hstring newName);
diff --git a/src/cascadia/TerminalSettingsEditor/ColorSchemes.h b/src/cascadia/TerminalSettingsEditor/ColorSchemes.h
index 63f755a6b26..9dbe211dbe7 100644
--- a/src/cascadia/TerminalSettingsEditor/ColorSchemes.h
+++ b/src/cascadia/TerminalSettingsEditor/ColorSchemes.h
@@ -20,7 +20,6 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
void AddNew_Click(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::RoutedEventArgs& e);
void ListView_PreviewKeyDown(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::Input::KeyRoutedEventArgs& e);
- void ListView_SelectionChanged(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::Controls::SelectionChangedEventArgs& e);
WINRT_PROPERTY(Model::ColorScheme, CurrentColorScheme, nullptr);
WINRT_OBSERVABLE_PROPERTY(Editor::ColorSchemesPageViewModel, ViewModel, _PropertyChangedHandlers, nullptr);
diff --git a/src/cascadia/TerminalSettingsEditor/ColorSchemes.xaml b/src/cascadia/TerminalSettingsEditor/ColorSchemes.xaml
index 4478cde2aff..7d2fad0d4d7 100644
--- a/src/cascadia/TerminalSettingsEditor/ColorSchemes.xaml
+++ b/src/cascadia/TerminalSettingsEditor/ColorSchemes.xaml
@@ -210,6 +210,7 @@
Visibility="{x:Bind IsDefaultScheme, Mode=OneWay}">
diff --git a/src/cascadia/TerminalSettingsEditor/PreviewConnection.cpp b/src/cascadia/TerminalSettingsEditor/PreviewConnection.cpp
index 0551fc06d42..2c33b1b0f4a 100644
--- a/src/cascadia/TerminalSettingsEditor/PreviewConnection.cpp
+++ b/src/cascadia/TerminalSettingsEditor/PreviewConnection.cpp
@@ -8,15 +8,20 @@
using namespace ::winrt::Microsoft::Terminal::TerminalConnection;
using namespace ::winrt::Windows::Foundation;
+static constexpr std::wstring_view PromptTextPlain{ L"C:\\> " };
+static constexpr std::wstring_view PromptTextPowerline{ L"\x1b[49;34m\xe0b6\x1b[1;97;44m C:\\ \x1b[m\x1b[46;34m\xe0b8\x1b[49;36m\xe0b8\x1b[m " };
+
// clang-format off
static constexpr std::wstring_view PreviewText{
+ L"\x001b"
+ L"c" // Hard Reset (RIS); on separate lines to avoid becoming 0x01BC
L"Windows Terminal\r\n"
- L"C:\\> \x1b[93m" L"git\x1b[m diff \x1b[90m-w\x1b[m\r\n"
+ L"{0}\x1b[93m" L"git\x1b[m diff \x1b[90m-w\x1b[m\r\n"
L"\x1b[1m" L"diff --git a/win b/win\x1b[m\r\n"
L"\x1b[36m@@ -1 +1 @@\x1b[m\r\n"
L"\x1b[31m- Windows Console\x1b[m\r\n"
L"\x1b[32m+ Windows Terminal!\x1b[m\r\n"
- L"C:\\> \x1b[93mWrite-Host \x1b[36m\"\xd83c\xdf2f!\"\x1b[1D\x1b[m"
+ L"{0}\x1b[93mWrite-Host \x1b[36m\"\xd83c\xdf2f!\"\x1b[1D\x1b[m"
};
// clang-format on
@@ -27,7 +32,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
void PreviewConnection::Start() noexcept
{
// Send the preview text
- _TerminalOutputHandlers(PreviewText);
+ _TerminalOutputHandlers(fmt::format(PreviewText, _displayPowerlineGlyphs ? PromptTextPowerline : PromptTextPlain));
}
void PreviewConnection::Initialize(const Windows::Foundation::Collections::ValueSet& /*settings*/) noexcept
@@ -45,4 +50,13 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
void PreviewConnection::Close() noexcept
{
}
+
+ void PreviewConnection::DisplayPowerlineGlyphs(bool d) noexcept
+ {
+ if (_displayPowerlineGlyphs != d)
+ {
+ _displayPowerlineGlyphs = d;
+ Start();
+ }
+ }
}
diff --git a/src/cascadia/TerminalSettingsEditor/PreviewConnection.h b/src/cascadia/TerminalSettingsEditor/PreviewConnection.h
index 1b8d980ad21..7958611ca5c 100644
--- a/src/cascadia/TerminalSettingsEditor/PreviewConnection.h
+++ b/src/cascadia/TerminalSettingsEditor/PreviewConnection.h
@@ -27,9 +27,14 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
void Resize(uint32_t rows, uint32_t columns) noexcept;
void Close() noexcept;
+ void DisplayPowerlineGlyphs(bool d) noexcept;
+
winrt::Microsoft::Terminal::TerminalConnection::ConnectionState State() const noexcept { return winrt::Microsoft::Terminal::TerminalConnection::ConnectionState::Connected; }
WINRT_CALLBACK(TerminalOutput, winrt::Microsoft::Terminal::TerminalConnection::TerminalOutputHandler);
TYPED_EVENT(StateChanged, winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection, IInspectable);
+
+ private:
+ bool _displayPowerlineGlyphs{ false };
};
}
diff --git a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.cpp b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.cpp
index 547813d255b..5f31f6be579 100644
--- a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.cpp
+++ b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.cpp
@@ -21,6 +21,8 @@ using namespace winrt::Microsoft::Terminal::Settings::Model;
namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
+ static Editor::Font _FontObjectForDWriteFont(IDWriteFontFamily* family);
+
Windows::Foundation::Collections::IObservableVector ProfileViewModel::_MonospaceFontList{ nullptr };
Windows::Foundation::Collections::IObservableVector ProfileViewModel::_FontList{ nullptr };
@@ -118,12 +120,8 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
com_ptr fontFamily;
THROW_IF_FAILED(fontCollection->GetFontFamily(i, fontFamily.put()));
- // get the font's localized names
- com_ptr localizedFamilyNames;
- THROW_IF_FAILED(fontFamily->GetFamilyNames(localizedFamilyNames.put()));
-
// construct a font entry for tracking
- if (const auto fontEntry{ _GetFont(localizedFamilyNames) })
+ if (const auto fontEntry{ _FontObjectForDWriteFont(fontFamily.get()) })
{
// check if the font is monospaced
try
@@ -159,7 +157,32 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
}
CATCH_LOG();
- Editor::Font ProfileViewModel::_GetFont(com_ptr localizedFamilyNames)
+ Editor::Font ProfileViewModel::FindFontWithLocalizedName(const winrt::hstring& name) noexcept
+ {
+ // look for the current font in our shown list of fonts
+ Editor::Font fallbackFont{ nullptr };
+ try
+ {
+ const auto& currentFontList{ CompleteFontList() };
+ for (const auto& font : currentFontList)
+ {
+ if (font.LocalizedName() == name)
+ {
+ return font;
+ }
+ else if (font.LocalizedName() == L"Cascadia Mono")
+ {
+ fallbackFont = font;
+ }
+ }
+ }
+ CATCH_LOG();
+
+ // we couldn't find the desired font, set to "Cascadia Mono" if we found that since it ships by default
+ return fallbackFont;
+ }
+
+ static Editor::Font _FontObjectForDWriteFont(IDWriteFontFamily* family)
{
// used for the font's name as an identifier (i.e. text block's font family property)
std::wstring nameID;
@@ -169,6 +192,10 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
std::wstring localizedName;
UINT32 localizedNameIndex;
+ // get the font's localized names
+ winrt::com_ptr localizedFamilyNames;
+ THROW_IF_FAILED(family->GetFamilyNames(localizedFamilyNames.put()));
+
// use our current locale to find the localized name
auto exists{ FALSE };
HRESULT hr;
@@ -211,7 +238,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
if (!nameID.empty() && !localizedName.empty())
{
- return make(nameID, localizedName);
+ return make(nameID, localizedName, family);
}
return nullptr;
}
diff --git a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.h b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.h
index 386c4876a14..0946d9146ad 100644
--- a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.h
+++ b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.h
@@ -34,6 +34,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
static void UpdateFontList() noexcept;
static Windows::Foundation::Collections::IObservableVector CompleteFontList() noexcept { return _FontList; };
static Windows::Foundation::Collections::IObservableVector MonospaceFontList() noexcept { return _MonospaceFontList; };
+ static Editor::Font FindFontWithLocalizedName(winrt::hstring const& name) noexcept;
ProfileViewModel(const Model::Profile& profile, const Model::CascadiaSettings& settings);
Model::TerminalSettings TermSettings() const;
@@ -123,8 +124,6 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
static Windows::Foundation::Collections::IObservableVector _MonospaceFontList;
static Windows::Foundation::Collections::IObservableVector _FontList;
- static Editor::Font _GetFont(com_ptr localizedFamilyNames);
-
Model::CascadiaSettings _appSettings;
Editor::AppearanceViewModel _unfocusedAppearanceViewModel;
};
diff --git a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.idl b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.idl
index f4dd21b9c7a..ab0bcbf2ffb 100644
--- a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.idl
+++ b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.idl
@@ -36,6 +36,7 @@ namespace Microsoft.Terminal.Settings.Editor
{
static Windows.Foundation.Collections.IObservableVector CompleteFontList { get; };
static Windows.Foundation.Collections.IObservableVector MonospaceFontList { get; };
+ static Font FindFontWithLocalizedName(String name);
Microsoft.Terminal.Settings.Model.TerminalSettings TermSettings { get; };
diff --git a/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.cpp b/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.cpp
index 599b3eef513..d045bc3cf20 100644
--- a/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.cpp
+++ b/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.cpp
@@ -19,6 +19,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
Profiles_Appearance::Profiles_Appearance()
{
InitializeComponent();
+ _previewConnection = winrt::make_self();
}
void Profiles_Appearance::OnNavigatedTo(const NavigationEventArgs& e)
@@ -36,7 +37,8 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
if (!_previewControl)
{
const auto settings = _Profile.TermSettings();
- _previewControl = Control::TermControl(settings, settings, make());
+ _previewConnection->DisplayPowerlineGlyphs(_looksLikePowerlineFont());
+ _previewControl = Control::TermControl(settings, settings, *_previewConnection);
_previewControl.IsEnabled(false);
_previewControl.AllowFocusWhenDisabled(false);
_previewControl.DisplayCursorWhileBlurred(true);
@@ -68,9 +70,25 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
_Profile.DeleteUnfocusedAppearance();
}
+ bool Profiles_Appearance::_looksLikePowerlineFont() const
+ {
+ if (_Profile && _Profile.DefaultAppearance())
+ {
+ if (const auto fontName = _Profile.DefaultAppearance().FontFace(); !fontName.empty())
+ {
+ if (const auto font = ProfileViewModel::FindFontWithLocalizedName(fontName))
+ {
+ return font.HasPowerlineCharacters();
+ }
+ }
+ }
+ return false;
+ }
+
void Profiles_Appearance::_onProfilePropertyChanged(const IInspectable&, const PropertyChangedEventArgs&) const
{
const auto settings = _Profile.TermSettings();
+ _previewConnection->DisplayPowerlineGlyphs(_looksLikePowerlineFont());
_previewControl.UpdateControlSettings(settings, settings);
}
}
diff --git a/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.h b/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.h
index 4e13f60c632..0eadfeb6a6e 100644
--- a/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.h
+++ b/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.h
@@ -5,6 +5,7 @@
#include "Profiles_Appearance.g.h"
#include "Utils.h"
+#include "PreviewConnection.h"
namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
{
@@ -26,7 +27,9 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
private:
void _onProfilePropertyChanged(const IInspectable&, const PropertyChangedEventArgs&) const;
+ bool _looksLikePowerlineFont() const;
+ winrt::com_ptr _previewConnection{ nullptr };
Microsoft::Terminal::Control::TermControl _previewControl{ nullptr };
Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker _ViewModelChangedRevoker;
Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker _AppearanceViewModelChangedRevoker;
diff --git a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp
index aaf1e1ca251..83830f30ac4 100644
--- a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp
+++ b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp
@@ -73,6 +73,7 @@ static constexpr std::string_view IdentifyWindowKey{ "identifyWindow" };
static constexpr std::string_view IdentifyWindowsKey{ "identifyWindows" };
static constexpr std::string_view RenameWindowKey{ "renameWindow" };
static constexpr std::string_view OpenWindowRenamerKey{ "openWindowRenamer" };
+static constexpr std::string_view SearchForTextKey{ "searchWeb" };
static constexpr std::string_view GlobalSummonKey{ "globalSummon" };
static constexpr std::string_view QuakeModeKey{ "quakeMode" };
static constexpr std::string_view FocusPaneKey{ "focusPane" };
@@ -405,6 +406,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{ ShortcutAction::RenameWindow, RS_(L"ResetWindowNameCommandKey") },
{ ShortcutAction::OpenWindowRenamer, RS_(L"OpenWindowRenamerCommandKey") },
{ ShortcutAction::GlobalSummon, MustGenerate },
+ { ShortcutAction::SearchForText, MustGenerate },
{ ShortcutAction::QuakeMode, RS_(L"QuakeModeCommandKey") },
{ ShortcutAction::FocusPane, MustGenerate },
{ ShortcutAction::OpenSystemMenu, RS_(L"OpenSystemMenuCommandKey") },
diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionArgs.cpp
index 061a336ecde..a4c570e3d33 100644
--- a/src/cascadia/TerminalSettingsModel/ActionArgs.cpp
+++ b/src/cascadia/TerminalSettingsModel/ActionArgs.cpp
@@ -38,6 +38,7 @@
#include "PrevTabArgs.g.cpp"
#include "NextTabArgs.g.cpp"
#include "RenameWindowArgs.g.cpp"
+#include "SearchForTextArgs.g.cpp"
#include "GlobalSummonArgs.g.cpp"
#include "FocusPaneArgs.g.cpp"
#include "ExportBufferArgs.g.cpp"
@@ -787,6 +788,14 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
return RS_(L"ResetWindowNameCommandKey");
}
+ winrt::hstring SearchForTextArgs::GenerateName() const
+ {
+ return winrt::hstring{
+ fmt::format(std::wstring_view(RS_(L"SearchForTextCommandKey")),
+ Windows::Foundation::Uri(QueryUrl()).Domain().c_str())
+ };
+ }
+
winrt::hstring GlobalSummonArgs::GenerateName() const
{
// GH#10210 - Is this action literally the same thing as the `quakeMode`
diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.h b/src/cascadia/TerminalSettingsModel/ActionArgs.h
index 422a4abd5f7..b13db557de5 100644
--- a/src/cascadia/TerminalSettingsModel/ActionArgs.h
+++ b/src/cascadia/TerminalSettingsModel/ActionArgs.h
@@ -40,6 +40,7 @@
#include "PrevTabArgs.g.h"
#include "NextTabArgs.g.h"
#include "RenameWindowArgs.g.h"
+#include "SearchForTextArgs.g.h"
#include "GlobalSummonArgs.g.h"
#include "FocusPaneArgs.g.h"
#include "ExportBufferArgs.g.h"
@@ -233,6 +234,10 @@ private: \
#define RENAME_WINDOW_ARGS(X) \
X(winrt::hstring, Name, "name", false, L"")
+////////////////////////////////////////////////////////////////////////////////
+#define SEARCH_FOR_TEXT_ARGS(X) \
+ X(winrt::hstring, QueryUrl, "queryUrl", false, L"")
+
////////////////////////////////////////////////////////////////////////////////
#define GLOBAL_SUMMON_ARGS(X) \
X(winrt::hstring, Name, "name", false, L"") \
@@ -719,6 +724,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
ACTION_ARGS_STRUCT(RenameWindowArgs, RENAME_WINDOW_ARGS);
+ ACTION_ARGS_STRUCT(SearchForTextArgs, SEARCH_FOR_TEXT_ARGS);
+
struct GlobalSummonArgs : public GlobalSummonArgsT
{
ACTION_ARG_BODY(GlobalSummonArgs, GLOBAL_SUMMON_ARGS)
diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.idl b/src/cascadia/TerminalSettingsModel/ActionArgs.idl
index 2c7e229329e..cddf0cb8f4f 100644
--- a/src/cascadia/TerminalSettingsModel/ActionArgs.idl
+++ b/src/cascadia/TerminalSettingsModel/ActionArgs.idl
@@ -365,6 +365,11 @@ namespace Microsoft.Terminal.Settings.Model
String Name { get; };
};
+ [default_interface] runtimeclass SearchForTextArgs : IActionArgs
+ {
+ String QueryUrl { get; };
+ };
+
[default_interface] runtimeclass GlobalSummonArgs : IActionArgs
{
String Name { get; };
diff --git a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h
index c557fdddf04..94bd120c780 100644
--- a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h
+++ b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h
@@ -85,6 +85,7 @@
ON_ALL_ACTIONS(IdentifyWindows) \
ON_ALL_ACTIONS(RenameWindow) \
ON_ALL_ACTIONS(OpenWindowRenamer) \
+ ON_ALL_ACTIONS(SearchForText) \
ON_ALL_ACTIONS(GlobalSummon) \
ON_ALL_ACTIONS(QuakeMode) \
ON_ALL_ACTIONS(FocusPane) \
@@ -116,6 +117,7 @@
ON_ALL_ACTIONS_WITH_ARGS(CopyText) \
ON_ALL_ACTIONS_WITH_ARGS(ExecuteCommandline) \
ON_ALL_ACTIONS_WITH_ARGS(FindMatch) \
+ ON_ALL_ACTIONS_WITH_ARGS(SearchForText) \
ON_ALL_ACTIONS_WITH_ARGS(GlobalSummon) \
ON_ALL_ACTIONS_WITH_ARGS(MoveFocus) \
ON_ALL_ACTIONS_WITH_ARGS(MovePane) \
diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl
index b13ed49f898..02b3788b1ed 100644
--- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl
+++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl
@@ -101,6 +101,7 @@ namespace Microsoft.Terminal.Settings.Model
INHERITABLE_SETTING(Boolean, EnableShellCompletionMenu);
INHERITABLE_SETTING(Boolean, IsolatedMode);
INHERITABLE_SETTING(Boolean, AllowHeadless);
+ INHERITABLE_SETTING(String, SearchWebDefaultQueryUrl);
Windows.Foundation.Collections.IMapView ColorSchemes();
void AddColorScheme(ColorScheme scheme);
diff --git a/src/cascadia/TerminalSettingsModel/KeyChordSerialization.cpp b/src/cascadia/TerminalSettingsModel/KeyChordSerialization.cpp
index 50d2d135552..32681150692 100644
--- a/src/cascadia/TerminalSettingsModel/KeyChordSerialization.cpp
+++ b/src/cascadia/TerminalSettingsModel/KeyChordSerialization.cpp
@@ -124,7 +124,7 @@ static int32_t parseNumericCode(const std::wstring_view& str, const std::wstring
static KeyChord _fromString(std::wstring_view wstr)
{
using nameToVkeyPair = std::pair;
- static const til::static_map nameToVkey{
+ static constinit til::static_map nameToVkey{
// The above VKEY_NAME_PAIRS macro contains a list of key-binding names for each virtual key.
// This god-awful macro inverts VKEY_NAME_PAIRS and creates a static map of key-binding names to virtual keys.
// clang-format off
@@ -240,7 +240,7 @@ static KeyChord _fromString(std::wstring_view wstr)
static std::wstring _toString(const KeyChord& chord)
{
using vkeyToNamePair = std::pair;
- static const til::static_map vkeyToName{
+ static constinit til::static_map vkeyToName{
// The above VKEY_NAME_PAIRS macro contains a list of key-binding strings for each virtual key.
// This macro picks the first (most preferred) name and creates a static map of virtual keys to key-binding names.
#define GENERATOR(vkey, name1, ...) vkeyToNamePair{ vkey, name1 },
diff --git a/src/cascadia/TerminalSettingsModel/MTSMSettings.h b/src/cascadia/TerminalSettingsModel/MTSMSettings.h
index 3e6e6ffbaa1..44c19aec22d 100644
--- a/src/cascadia/TerminalSettingsModel/MTSMSettings.h
+++ b/src/cascadia/TerminalSettingsModel/MTSMSettings.h
@@ -66,7 +66,8 @@ Author(s):
X(bool, EnableShellCompletionMenu, "experimental.enableShellCompletionMenu", false) \
X(winrt::Windows::Foundation::Collections::IVector, NewTabMenu, "newTabMenu", winrt::single_threaded_vector({ Model::RemainingProfilesEntry{} })) \
X(bool, AllowHeadless, "compatibility.allowHeadless", false) \
- X(bool, IsolatedMode, "compatibility.isolatedMode", false)
+ X(bool, IsolatedMode, "compatibility.isolatedMode", false) \
+ X(hstring, SearchWebDefaultQueryUrl, "searchWebDefaultQueryUrl", L"https://www.bing.com/search?q=%22%s%22")
#define MTSM_PROFILE_SETTINGS(X) \
X(int32_t, HistorySize, "historySize", DEFAULT_HISTORY_SIZE) \
@@ -133,6 +134,9 @@ Author(s):
#define MTSM_THEME_WINDOW_SETTINGS(X) \
X(winrt::Windows::UI::Xaml::ElementTheme, RequestedTheme, "applicationTheme", winrt::Windows::UI::Xaml::ElementTheme::Default) \
+ X(winrt::Microsoft::Terminal::Settings::Model::ThemeColor, Frame, "frame", nullptr) \
+ X(winrt::Microsoft::Terminal::Settings::Model::ThemeColor, UnfocusedFrame, "unfocusedFrame", nullptr) \
+ X(bool, RainbowFrame, "experimental.rainbowFrame", false) \
X(bool, UseMica, "useMica", false)
#define MTSM_THEME_TABROW_SETTINGS(X) \
diff --git a/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj b/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj
index 43f77d6eb1d..2f77b745661 100644
--- a/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj
+++ b/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj
@@ -327,7 +327,7 @@
-
+
diff --git a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw
index ef205e73c99..e8334321636 100644
--- a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw
+++ b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw
@@ -705,4 +705,12 @@
Select previous command
+
+ Search {0}
+ {0} will be replaced with a user-specified URL to a web page
+
+
+ Search the web for selected text
+ This will open a web browser to search for some user-selected text
+
diff --git a/src/cascadia/TerminalSettingsModel/Theme.idl b/src/cascadia/TerminalSettingsModel/Theme.idl
index 4c2338ac604..f7f041a9dda 100644
--- a/src/cascadia/TerminalSettingsModel/Theme.idl
+++ b/src/cascadia/TerminalSettingsModel/Theme.idl
@@ -49,6 +49,9 @@ namespace Microsoft.Terminal.Settings.Model
runtimeclass WindowTheme {
Windows.UI.Xaml.ElementTheme RequestedTheme { get; };
Boolean UseMica { get; };
+ Boolean RainbowFrame { get; };
+ ThemeColor Frame { get; };
+ ThemeColor UnfocusedFrame { get; };
}
runtimeclass TabRowTheme {
diff --git a/src/cascadia/TerminalSettingsModel/defaults.json b/src/cascadia/TerminalSettingsModel/defaults.json
index 16246378aaa..117abb5c892 100644
--- a/src/cascadia/TerminalSettingsModel/defaults.json
+++ b/src/cascadia/TerminalSettingsModel/defaults.json
@@ -466,6 +466,9 @@
{ "command": "expandSelectionToWord" },
{ "command": "showContextMenu", "keys": "menu" },
+ // Web Search
+ { "command": { "action": "searchWeb" }, "name": { "key": "SearchWebCommandKey" } },
+
// Scrollback
{ "command": "scrollDown", "keys": "ctrl+shift+down" },
{ "command": "scrollDownPage", "keys": "ctrl+shift+pgdn" },
diff --git a/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp b/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp
index dc66b3a03ed..9f267746511 100644
--- a/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp
+++ b/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp
@@ -21,7 +21,7 @@
#include "../host/readDataCooked.hpp"
#include "../host/output.h"
#include "../host/_stream.h" // For WriteCharsLegacy
-#include "../host/cmdline.h" // For WC_LIMIT_BACKSPACE
+#include "../host/cmdline.h" // For WC_INTERACTIVE
#include "test/CommonState.hpp"
#include "../cascadia/TerminalCore/Terminal.hpp"
@@ -1200,7 +1200,9 @@ void ConptyRoundtripTests::PassthroughHardReset()
}
// Write a Hard Reset VT sequence to the host, it should come through to the Terminal
+ // along with a DECSET sequence to re-enable win32 input and focus events.
expectedOutput.push_back("\033c");
+ expectedOutput.push_back("\033[?9001;1004h");
hostSm.ProcessString(L"\033c");
const auto termSecondView = term->GetViewport();
@@ -3420,7 +3422,7 @@ void ConptyRoundtripTests::WrapNewLineAtBottomLikeMSYS()
}
else if (writingMethod == PrintWithWriteCharsLegacy)
{
- doWriteCharsLegacy(si, str, WC_LIMIT_BACKSPACE);
+ doWriteCharsLegacy(si, str, WC_INTERACTIVE);
}
};
diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp
index ccc52ead0cd..9d2066a5dac 100644
--- a/src/cascadia/WindowsTerminal/AppHost.cpp
+++ b/src/cascadia/WindowsTerminal/AppHost.cpp
@@ -28,6 +28,8 @@ using namespace std::chrono_literals;
// "If the high-order bit is 1, the key is down; otherwise, it is up."
static constexpr short KeyPressed{ gsl::narrow_cast(0x8000) };
+constexpr const auto FrameUpdateInterval = std::chrono::milliseconds(16);
+
AppHost::AppHost(const winrt::TerminalApp::AppLogic& logic,
winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs args,
const Remoting::WindowManager& manager,
@@ -38,6 +40,8 @@ AppHost::AppHost(const winrt::TerminalApp::AppLogic& logic,
_peasant{ peasant },
_desktopManager{ winrt::try_create_instance(__uuidof(VirtualDesktopManager)) }
{
+ _started = std::chrono::high_resolution_clock::now();
+
_HandleCommandlineArgs(args);
_HandleSessionRestore(!args.Content().empty());
@@ -419,6 +423,10 @@ void AppHost::Close()
// After calling _window->Close() we should avoid creating more WinUI related actions.
// I suspect WinUI wouldn't like that very much. As such unregister all event handlers first.
_revokers = {};
+ if (_frameTimer)
+ {
+ _frameTimer.Tick(_frameTimerToken);
+ }
_showHideWindowThrottler.reset();
_window->Close();
@@ -1036,24 +1044,138 @@ static bool _isActuallyDarkTheme(const auto requestedTheme)
}
}
+// DwmSetWindowAttribute(... DWMWA_BORDER_COLOR.. ) doesn't work on Windows 10,
+// but it _will_ spew to the debug console. This helper just no-ops the call on
+// Windows 10, so that we don't even get that spew
+void _frameColorHelper(const HWND h, const COLORREF color)
+{
+ static const bool isWindows11 = []() {
+ OSVERSIONINFOEXW osver{};
+ osver.dwOSVersionInfoSize = sizeof(osver);
+ osver.dwBuildNumber = 22000;
+
+ DWORDLONG dwlConditionMask = 0;
+ VER_SET_CONDITION(dwlConditionMask, VER_BUILDNUMBER, VER_GREATER_EQUAL);
+
+ if (VerifyVersionInfoW(&osver, VER_BUILDNUMBER, dwlConditionMask) != FALSE)
+ {
+ return true;
+ }
+ return false;
+ }();
+
+ if (isWindows11)
+ {
+ LOG_IF_FAILED(DwmSetWindowAttribute(h, DWMWA_BORDER_COLOR, &color, sizeof(color)));
+ }
+}
+
void AppHost::_updateTheme()
{
auto theme = _appLogic.Theme();
_window->OnApplicationThemeChanged(theme.RequestedTheme());
+ const auto windowTheme{ theme.Window() };
+
const auto b = _windowLogic.TitlebarBrush();
const auto color = ThemeColor::ColorFromBrush(b);
const auto colorOpacity = b ? color.A / 255.0 : 0.0;
const auto brushOpacity = _opacityFromBrush(b);
const auto opacity = std::min(colorOpacity, brushOpacity);
- _window->UseMica(theme.Window() ? theme.Window().UseMica() : false, opacity);
+ _window->UseMica(windowTheme ? windowTheme.UseMica() : false, opacity);
// This is a hack to make the window borders dark instead of light.
// It must be done before WM_NCPAINT so that the borders are rendered with
// the correct theme.
// For more information, see GH#6620.
LOG_IF_FAILED(TerminalTrySetDarkTheme(_window->GetHandle(), _isActuallyDarkTheme(theme.RequestedTheme())));
+
+ // Update the window frame. If `rainbowFrame:true` is enabled, then that
+ // will be used. Otherwise we'll try to use the `FrameBrush` set in the
+ // terminal window, as that will have the right color for the ThemeColor for
+ // this setting. If that value is null, then revert to the default frame
+ // color.
+ if (windowTheme)
+ {
+ if (windowTheme.RainbowFrame())
+ {
+ _startFrameTimer();
+ }
+ else if (const auto b{ _windowLogic.FrameBrush() })
+ {
+ _stopFrameTimer();
+ const auto color = ThemeColor::ColorFromBrush(b);
+ COLORREF ref{ til::color{ color } };
+ _frameColorHelper(_window->GetHandle(), ref);
+ }
+ else
+ {
+ _stopFrameTimer();
+ // DWMWA_COLOR_DEFAULT is the magic "reset to the default" value
+ _frameColorHelper(_window->GetHandle(), DWMWA_COLOR_DEFAULT);
+ }
+ }
+}
+
+void AppHost::_startFrameTimer()
+{
+ // Instantiate the frame color timer, if we haven't already. We'll only ever
+ // create one instance of this. We'll set up the callback for the timers as
+ // _updateFrameColor, which will actually handle setting the colors. If we
+ // already have a timer, just start that one.
+
+ if (_frameTimer == nullptr)
+ {
+ _frameTimer = winrt::Windows::UI::Xaml::DispatcherTimer();
+ _frameTimer.Interval(FrameUpdateInterval);
+ _frameTimerToken = _frameTimer.Tick({ this, &AppHost::_updateFrameColor });
+ }
+ _frameTimer.Start();
+}
+
+void AppHost::_stopFrameTimer()
+{
+ if (_frameTimer)
+ {
+ _frameTimer.Stop();
+ }
+}
+
+// Method Description:
+// - Updates the color of the window frame to cycle through all the colors. This
+// is called as the `_frameTimer` Tick callback, roughly 60 times per second.
+void AppHost::_updateFrameColor(const winrt::Windows::Foundation::IInspectable&, const winrt::Windows::Foundation::IInspectable&)
+{
+ // First, a couple helper functions:
+ static const auto saturateAndToColor = [](const float a, const float b, const float c) -> til::color {
+ return til::color{
+ base::saturated_cast(255.f * std::clamp(a, 0.f, 1.f)),
+ base::saturated_cast(255.f * std::clamp(b, 0.f, 1.f)),
+ base::saturated_cast(255.f * std::clamp(c, 0.f, 1.f))
+ };
+ };
+
+ // Helper for converting a hue [0, 1) to an RGB value.
+ // Credit to https://www.chilliant.com/rgb2hsv.html
+ static const auto hueToRGB = [&](const float H) -> til::color {
+ float R = abs(H * 6 - 3) - 1;
+ float G = 2 - abs(H * 6 - 2);
+ float B = 2 - abs(H * 6 - 4);
+ return saturateAndToColor(R, G, B);
+ };
+
+ // Now, the main body of work.
+ // - Convert the time delta between when we were started and now, to a hue. This will cycle us through all the colors.
+ // - Convert that hue to an RGB value.
+ // - Set the frame's color to that RGB color.
+ const auto now = std::chrono::high_resolution_clock::now();
+ const std::chrono::duration delta{ now - _started };
+ const auto seconds = delta.count() / 4; // divide by four, to make the effect slower. Otherwise it flashes way to fast.
+ float ignored;
+ const auto color = hueToRGB(modf(seconds, &ignored));
+
+ _frameColorHelper(_window->GetHandle(), color);
}
void AppHost::_HandleSettingsChanged(const winrt::Windows::Foundation::IInspectable& /*sender*/,
@@ -1203,6 +1325,10 @@ void AppHost::_PropertyChangedHandler(const winrt::Windows::Foundation::IInspect
_updateTheme();
}
}
+ else if (e.PropertyName() == L"FrameBrush")
+ {
+ _updateTheme();
+ }
}
winrt::fire_and_forget AppHost::_WindowInitializedHandler(const winrt::Windows::Foundation::IInspectable& /*sender*/,
diff --git a/src/cascadia/WindowsTerminal/AppHost.h b/src/cascadia/WindowsTerminal/AppHost.h
index 5cd639fd5ed..27b71af865d 100644
--- a/src/cascadia/WindowsTerminal/AppHost.h
+++ b/src/cascadia/WindowsTerminal/AppHost.h
@@ -51,6 +51,9 @@ class AppHost
std::shared_ptr> _showHideWindowThrottler;
+ std::chrono::time_point _started;
+ winrt::Windows::UI::Xaml::DispatcherTimer _frameTimer{ nullptr };
+
uint32_t _launchShowWindowCommand{ SW_NORMAL };
void _preInit();
@@ -151,7 +154,12 @@ class AppHost
void _handleSendContent(const winrt::Windows::Foundation::IInspectable& sender,
winrt::Microsoft::Terminal::Remoting::RequestReceiveContentArgs args);
+ void _startFrameTimer();
+ void _stopFrameTimer();
+ void _updateFrameColor(const winrt::Windows::Foundation::IInspectable&, const winrt::Windows::Foundation::IInspectable&);
+
winrt::event_token _GetWindowLayoutRequestedToken;
+ winrt::event_token _frameTimerToken;
// Helper struct. By putting these all into one struct, we can revoke them
// all at once, by assigning _revokers to a fresh Revokers instance. That'll
diff --git a/src/cascadia/WindowsTerminal_UIATests/SmokeTests.cs b/src/cascadia/WindowsTerminal_UIATests/SmokeTests.cs
index f20c0061f81..8c643391fdb 100644
--- a/src/cascadia/WindowsTerminal_UIATests/SmokeTests.cs
+++ b/src/cascadia/WindowsTerminal_UIATests/SmokeTests.cs
@@ -175,5 +175,20 @@ public void RunMakeKillTabs()
Globals.WaitForLongTimeout();
}
}
+
+ [TestMethod]
+ [TestProperty("IsPGO", "true")]
+ public void RunOpenSettingsUI()
+ {
+ using (TerminalApp app = new TerminalApp(TestContext))
+ {
+ var root = app.GetRoot();
+
+ root.SendKeys(Keys.LeftControl + ",");
+ Globals.WaitForTimeout();
+
+ Globals.WaitForLongTimeout();
+ }
+ }
}
}
diff --git a/src/cascadia/WindowsTerminal_UIATests/WindowsTerminal.UIA.Tests.csproj b/src/cascadia/WindowsTerminal_UIATests/WindowsTerminal.UIA.Tests.csproj
index 6e872176954..589cb6f3af4 100644
--- a/src/cascadia/WindowsTerminal_UIATests/WindowsTerminal.UIA.Tests.csproj
+++ b/src/cascadia/WindowsTerminal_UIATests/WindowsTerminal.UIA.Tests.csproj
@@ -126,7 +126,7 @@
- copy $(SolutionDir)\dep\WinAppDriver\* $(OutDir)\
+ copy "$(SolutionDir)\dep\WinAppDriver\*" "$(OutDir)\"
\ No newline at end of file
diff --git a/src/common.build.pre.props b/src/common.build.pre.props
index 5d4c2a6f81d..1cf31b37e44 100644
--- a/src/common.build.pre.props
+++ b/src/common.build.pre.props
@@ -120,11 +120,12 @@
C26445: Do not assign std::span or std::string_view to a reference. They are cheap to construct and are not owners of the underlying data. (gsl.view).
Even for MSVC v19.32 this is actually far from true. Copying (as opposed to referencing) larger
than register-sized structures is fairly expensive. Example: https://godbolt.org/z/oPco88PaP
- C26813: Use 'bitwise and' to check if a flag is set.
- The MSVC v19.31 toolset has a bug where a pointer to an enum is incorrectly flagged with C26813.
- It's supposed to be fixed with VS 17.2.1 and 17.3.0 and later respectively.
+ C26434: Method 'A' hides a non-virtual method 'B' (c.128).
+ C26456: Operator 'A' hides a non-virtual operator 'B' (c.128)
+ I think these rules are for when you fully bought into OOP?
+ We didn't and it breaks WRL and large parts of conhost code.
-->
- 4201;4312;4467;5105;26445;26813;%(DisableSpecificWarnings)
+ 4201;4312;4467;5105;26434;26445;26456;%(DisableSpecificWarnings)_WINDOWS;EXTERNAL_BUILD;%(PreprocessorDefinitions)trueprecomp.h
@@ -135,10 +136,10 @@
falsetruetrue
+ truestdcpp20stdc17
-
- %(AdditionalOptions) /utf-8 /Zc:externConstexpr /Zc:lambda /Zc:throwingNew /fp:contract
+ %(AdditionalOptions) /utf-8 /Zc:__cplusplus /Zc:__STDC__ /Zc:enumTypes /Zc:externConstexpr /Zc:templateScope /Zc:throwingNewGuard
diff --git a/src/common.nugetversions.targets b/src/common.nugetversions.targets
index 0f915d51469..bc949206615 100644
--- a/src/common.nugetversions.targets
+++ b/src/common.nugetversions.targets
@@ -27,7 +27,7 @@
Condition="'$(BuildingInsideVisualStudio)' == 'true'">
-
+ >& outEvents,
- const size_t eventsToRead,
- INPUT_READ_HANDLE_DATA& readHandleState,
- std::unique_ptr& waiter) noexcept override;
-
- [[nodiscard]] HRESULT PeekConsoleInputWImpl(IConsoleInputObject& context,
- std::deque>& outEvents,
- const size_t eventsToRead,
- INPUT_READ_HANDLE_DATA& readHandleState,
- std::unique_ptr& waiter) noexcept override;
-
- [[nodiscard]] HRESULT ReadConsoleInputAImpl(IConsoleInputObject& context,
- std::deque>& outEvents,
- const size_t eventsToRead,
- INPUT_READ_HANDLE_DATA& readHandleState,
- std::unique_ptr& waiter) noexcept override;
-
- [[nodiscard]] HRESULT ReadConsoleInputWImpl(IConsoleInputObject& context,
- std::deque>& outEvents,
- const size_t eventsToRead,
- INPUT_READ_HANDLE_DATA& readHandleState,
- std::unique_ptr& waiter) noexcept override;
+ [[nodiscard]] HRESULT GetConsoleInputImpl(IConsoleInputObject& context,
+ InputEventQueue& outEvents,
+ const size_t eventReadCount,
+ INPUT_READ_HANDLE_DATA& readHandleState,
+ const bool IsUnicode,
+ const bool IsPeek,
+ std::unique_ptr& waiter) noexcept override;
[[nodiscard]] HRESULT ReadConsoleAImpl(IConsoleInputObject& context,
std::span buffer,
diff --git a/src/host/CommandNumberPopup.cpp b/src/host/CommandNumberPopup.cpp
index bf77a39b37f..4ccce4d6d75 100644
--- a/src/host/CommandNumberPopup.cpp
+++ b/src/host/CommandNumberPopup.cpp
@@ -42,7 +42,7 @@ void CommandNumberPopup::_handleNumber(COOKED_READ_DATA& cookedReadData, const w
&CharsToWrite,
&NumSpaces,
cookedReadData.OriginalCursorPosition().x,
- WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
+ WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE,
nullptr));
cookedReadData.ScreenInfo().SetAttributes(realAttributes);
try
@@ -73,7 +73,7 @@ void CommandNumberPopup::_handleBackspace(COOKED_READ_DATA& cookedReadData) noex
&CharsToWrite,
&NumSpaces,
cookedReadData.OriginalCursorPosition().x,
- WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
+ WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE,
nullptr));
cookedReadData.ScreenInfo().SetAttributes(realAttributes);
_pop();
diff --git a/src/host/ConsoleArguments.cpp b/src/host/ConsoleArguments.cpp
index 2541cc8ab82..3621bca387f 100644
--- a/src/host/ConsoleArguments.cpp
+++ b/src/host/ConsoleArguments.cpp
@@ -20,7 +20,6 @@ const std::wstring_view ConsoleArguments::WIDTH_ARG = L"--width";
const std::wstring_view ConsoleArguments::HEIGHT_ARG = L"--height";
const std::wstring_view ConsoleArguments::INHERIT_CURSOR_ARG = L"--inheritcursor";
const std::wstring_view ConsoleArguments::RESIZE_QUIRK = L"--resizeQuirk";
-const std::wstring_view ConsoleArguments::WIN32_INPUT_MODE = L"--win32input";
const std::wstring_view ConsoleArguments::FEATURE_ARG = L"--feature";
const std::wstring_view ConsoleArguments::FEATURE_PTY_ARG = L"pty";
const std::wstring_view ConsoleArguments::COM_SERVER_ARG = L"-Embedding";
@@ -515,12 +514,6 @@ void ConsoleArguments::s_ConsumeArg(_Inout_ std::vector& args, _In
s_ConsumeArg(args, i);
hr = S_OK;
}
- else if (arg == WIN32_INPUT_MODE)
- {
- _win32InputMode = true;
- s_ConsumeArg(args, i);
- hr = S_OK;
- }
else if (arg == CLIENT_COMMANDLINE_ARG)
{
// Everything after this is the explicit commandline
@@ -677,10 +670,6 @@ bool ConsoleArguments::IsResizeQuirkEnabled() const
{
return _resizeQuirk;
}
-bool ConsoleArguments::IsWin32InputModeEnabled() const
-{
- return _win32InputMode;
-}
#ifdef UNIT_TESTING
// Method Description:
diff --git a/src/host/ConsoleArguments.hpp b/src/host/ConsoleArguments.hpp
index bb4a95b21e6..8648c92ea66 100644
--- a/src/host/ConsoleArguments.hpp
+++ b/src/host/ConsoleArguments.hpp
@@ -55,7 +55,6 @@ class ConsoleArguments
short GetHeight() const;
bool GetInheritCursor() const;
bool IsResizeQuirkEnabled() const;
- bool IsWin32InputModeEnabled() const;
#ifdef UNIT_TESTING
void EnableConptyModeForTests();
@@ -74,7 +73,6 @@ class ConsoleArguments
static const std::wstring_view HEIGHT_ARG;
static const std::wstring_view INHERIT_CURSOR_ARG;
static const std::wstring_view RESIZE_QUIRK;
- static const std::wstring_view WIN32_INPUT_MODE;
static const std::wstring_view FEATURE_ARG;
static const std::wstring_view FEATURE_PTY_ARG;
static const std::wstring_view COM_SERVER_ARG;
@@ -144,7 +142,6 @@ class ConsoleArguments
DWORD _signalHandle;
bool _inheritCursor;
bool _resizeQuirk{ false };
- bool _win32InputMode{ false };
[[nodiscard]] HRESULT _GetClientCommandline(_Inout_ std::vector& args,
const size_t index,
diff --git a/src/host/VtApiRoutines.cpp b/src/host/VtApiRoutines.cpp
index e3d4c59aea4..ff71f8c3a5b 100644
--- a/src/host/VtApiRoutines.cpp
+++ b/src/host/VtApiRoutines.cpp
@@ -105,46 +105,16 @@ void VtApiRoutines::_SynchronizeCursor(std::unique_ptr& waiter) no
}
}
-[[nodiscard]] HRESULT VtApiRoutines::PeekConsoleInputAImpl(IConsoleInputObject& context,
- std::deque>& outEvents,
- const size_t eventsToRead,
- INPUT_READ_HANDLE_DATA& readHandleState,
- std::unique_ptr& waiter) noexcept
-{
- const auto hr = m_pUsualRoutines->PeekConsoleInputAImpl(context, outEvents, eventsToRead, readHandleState, waiter);
- _SynchronizeCursor(waiter);
- return hr;
-}
-
-[[nodiscard]] HRESULT VtApiRoutines::PeekConsoleInputWImpl(IConsoleInputObject& context,
- std::deque>& outEvents,
- const size_t eventsToRead,
- INPUT_READ_HANDLE_DATA& readHandleState,
- std::unique_ptr& waiter) noexcept
-{
- const auto hr = m_pUsualRoutines->PeekConsoleInputWImpl(context, outEvents, eventsToRead, readHandleState, waiter);
- _SynchronizeCursor(waiter);
- return hr;
-}
-
-[[nodiscard]] HRESULT VtApiRoutines::ReadConsoleInputAImpl(IConsoleInputObject& context,
- std::deque>& outEvents,
- const size_t eventsToRead,
- INPUT_READ_HANDLE_DATA& readHandleState,
- std::unique_ptr& waiter) noexcept
-{
- const auto hr = m_pUsualRoutines->ReadConsoleInputAImpl(context, outEvents, eventsToRead, readHandleState, waiter);
- _SynchronizeCursor(waiter);
- return hr;
-}
-
-[[nodiscard]] HRESULT VtApiRoutines::ReadConsoleInputWImpl(IConsoleInputObject& context,
- std::deque>& outEvents,
- const size_t eventsToRead,
- INPUT_READ_HANDLE_DATA& readHandleState,
- std::unique_ptr& waiter) noexcept
-{
- const auto hr = m_pUsualRoutines->ReadConsoleInputWImpl(context, outEvents, eventsToRead, readHandleState, waiter);
+[[nodiscard]] HRESULT VtApiRoutines::GetConsoleInputImpl(
+ IConsoleInputObject& context,
+ InputEventQueue& outEvents,
+ const size_t eventReadCount,
+ INPUT_READ_HANDLE_DATA& readHandleState,
+ const bool IsUnicode,
+ const bool IsPeek,
+ std::unique_ptr& waiter) noexcept
+{
+ const auto hr = m_pUsualRoutines->GetConsoleInputImpl(context, outEvents, eventReadCount, readHandleState, IsUnicode, IsPeek, waiter);
_SynchronizeCursor(waiter);
return hr;
}
diff --git a/src/host/VtApiRoutines.h b/src/host/VtApiRoutines.h
index 2c95d340c97..8c3cbf917ef 100644
--- a/src/host/VtApiRoutines.h
+++ b/src/host/VtApiRoutines.h
@@ -52,29 +52,13 @@ class VtApiRoutines : public IApiRoutines
[[nodiscard]] HRESULT GetNumberOfConsoleInputEventsImpl(const InputBuffer& context,
ULONG& events) noexcept override;
- [[nodiscard]] HRESULT PeekConsoleInputAImpl(IConsoleInputObject& context,
- std::deque>& outEvents,
- const size_t eventsToRead,
- INPUT_READ_HANDLE_DATA& readHandleState,
- std::unique_ptr& waiter) noexcept override;
-
- [[nodiscard]] HRESULT PeekConsoleInputWImpl(IConsoleInputObject& context,
- std::deque>& outEvents,
- const size_t eventsToRead,
- INPUT_READ_HANDLE_DATA& readHandleState,
- std::unique_ptr& waiter) noexcept override;
-
- [[nodiscard]] HRESULT ReadConsoleInputAImpl(IConsoleInputObject& context,
- std::deque>& outEvents,
- const size_t eventsToRead,
- INPUT_READ_HANDLE_DATA& readHandleState,
- std::unique_ptr& waiter) noexcept override;
-
- [[nodiscard]] HRESULT ReadConsoleInputWImpl(IConsoleInputObject& context,
- std::deque>& outEvents,
- const size_t eventsToRead,
- INPUT_READ_HANDLE_DATA& readHandleState,
- std::unique_ptr& waiter) noexcept override;
+ [[nodiscard]] HRESULT GetConsoleInputImpl(IConsoleInputObject& context,
+ InputEventQueue& outEvents,
+ const size_t eventReadCount,
+ INPUT_READ_HANDLE_DATA& readHandleState,
+ const bool IsUnicode,
+ const bool IsPeek,
+ std::unique_ptr& waiter) noexcept override;
[[nodiscard]] HRESULT ReadConsoleAImpl(IConsoleInputObject& context,
std::span buffer,
diff --git a/src/host/VtIo.cpp b/src/host/VtIo.cpp
index b0f4ebad2c3..5d33e607931 100644
--- a/src/host/VtIo.cpp
+++ b/src/host/VtIo.cpp
@@ -71,7 +71,6 @@ VtIo::VtIo() :
{
_lookingForCursorPosition = pArgs->GetInheritCursor();
_resizeQuirk = pArgs->IsResizeQuirkEnabled();
- _win32InputMode = pArgs->IsWin32InputModeEnabled();
_passthroughMode = pArgs->IsPassthroughMode();
// If we were already given VT handles, set up the VT IO engine to use those.
@@ -269,10 +268,7 @@ bool VtIo::IsUsingVt() const
// win32-input-mode from them. This will enable the connected terminal to
// send us full INPUT_RECORDs as input. If the terminal doesn't understand
// this sequence, it'll just ignore it.
- if (_win32InputMode)
- {
- LOG_IF_FAILED(_pVtRenderEngine->RequestWin32Input());
- }
+ LOG_IF_FAILED(_pVtRenderEngine->RequestWin32Input());
// MSFT: 15813316
// If the terminal application wants us to inherit the cursor position,
diff --git a/src/host/VtIo.hpp b/src/host/VtIo.hpp
index e47d37d9ef6..a15653ad084 100644
--- a/src/host/VtIo.hpp
+++ b/src/host/VtIo.hpp
@@ -67,7 +67,6 @@ namespace Microsoft::Console::VirtualTerminal
bool _lookingForCursorPosition;
bool _resizeQuirk{ false };
- bool _win32InputMode{ false };
bool _passthroughMode{ false };
bool _closeEventSent{ false };
diff --git a/src/host/_stream.cpp b/src/host/_stream.cpp
index d6f1e87e80e..d85d4e105f8 100644
--- a/src/host/_stream.cpp
+++ b/src/host/_stream.cpp
@@ -39,10 +39,7 @@ using Microsoft::Console::VirtualTerminal::StateMachine;
// - coordCursor - New location of cursor.
// - fKeepCursorVisible - TRUE if changing window origin desirable when hit right edge
// Return Value:
-[[nodiscard]] NTSTATUS AdjustCursorPosition(SCREEN_INFORMATION& screenInfo,
- _In_ til::point coordCursor,
- const BOOL fKeepCursorVisible,
- _Inout_opt_ til::CoordType* psScrollY)
+void AdjustCursorPosition(SCREEN_INFORMATION& screenInfo, _In_ til::point coordCursor, const BOOL fKeepCursorVisible, _Inout_opt_ til::CoordType* psScrollY)
{
const auto bufferSize = screenInfo.GetBufferSize().Dimensions();
if (coordCursor.x < 0)
@@ -71,16 +68,10 @@ using Microsoft::Console::VirtualTerminal::StateMachine;
}
}
- auto Status = STATUS_SUCCESS;
-
if (coordCursor.y >= bufferSize.height)
{
// At the end of the buffer. Scroll contents of screen buffer so new position is visible.
- FAIL_FAST_IF(!(coordCursor.y == bufferSize.height));
- if (!StreamScrollRegion(screenInfo))
- {
- Status = STATUS_NO_MEMORY;
- }
+ StreamScrollRegion(screenInfo);
if (nullptr != psScrollY)
{
@@ -90,28 +81,61 @@ using Microsoft::Console::VirtualTerminal::StateMachine;
}
const auto cursorMovedPastViewport = coordCursor.y > screenInfo.GetViewport().BottomInclusive();
- if (SUCCEEDED_NTSTATUS(Status))
+
+ // if at right or bottom edge of window, scroll right or down one char.
+ if (cursorMovedPastViewport)
{
- // if at right or bottom edge of window, scroll right or down one char.
- if (cursorMovedPastViewport)
- {
- til::point WindowOrigin;
- WindowOrigin.x = 0;
- WindowOrigin.y = coordCursor.y - screenInfo.GetViewport().BottomInclusive();
- Status = screenInfo.SetViewportOrigin(false, WindowOrigin, true);
- }
+ til::point WindowOrigin;
+ WindowOrigin.x = 0;
+ WindowOrigin.y = coordCursor.y - screenInfo.GetViewport().BottomInclusive();
+ LOG_IF_FAILED(screenInfo.SetViewportOrigin(false, WindowOrigin, true));
}
- if (SUCCEEDED_NTSTATUS(Status))
+ if (fKeepCursorVisible)
{
- if (fKeepCursorVisible)
+ screenInfo.MakeCursorVisible(coordCursor);
+ }
+ LOG_IF_FAILED(screenInfo.SetCursorPosition(coordCursor, !!fKeepCursorVisible));
+}
+
+// As the name implies, this writes text without processing its control characters.
+static size_t _writeCharsLegacyUnprocessed(SCREEN_INFORMATION& screenInfo, const DWORD dwFlags, til::CoordType* const psScrollY, const std::wstring_view& text)
+{
+ const auto keepCursorVisible = WI_IsFlagSet(dwFlags, WC_KEEP_CURSOR_VISIBLE);
+ const auto wrapAtEOL = WI_IsFlagSet(screenInfo.OutputMode, ENABLE_WRAP_AT_EOL_OUTPUT);
+ const auto hasAccessibilityEventing = screenInfo.HasAccessibilityEventing();
+ auto& textBuffer = screenInfo.GetTextBuffer();
+ size_t numSpaces = 0;
+
+ RowWriteState state{
+ .text = text,
+ .columnLimit = textBuffer.GetSize().RightExclusive(),
+ };
+
+ while (!state.text.empty())
+ {
+ auto cursorPosition = textBuffer.GetCursor().GetPosition();
+
+ state.columnBegin = cursorPosition.x;
+ textBuffer.Write(cursorPosition.y, textBuffer.GetCurrentAttributes(), state);
+ cursorPosition.x = state.columnEnd;
+
+ numSpaces += gsl::narrow_cast(state.columnEnd - state.columnBegin);
+
+ if (wrapAtEOL && state.columnEnd >= state.columnLimit)
+ {
+ textBuffer.SetWrapForced(cursorPosition.y, true);
+ }
+
+ if (hasAccessibilityEventing && state.columnEnd > state.columnBegin)
{
- screenInfo.MakeCursorVisible(coordCursor);
+ screenInfo.NotifyAccessibilityEventing(state.columnBegin, cursorPosition.y, state.columnEnd - 1, cursorPosition.y);
}
- Status = screenInfo.SetCursorPosition(coordCursor, !!fKeepCursorVisible);
+
+ AdjustCursorPosition(screenInfo, cursorPosition, keepCursorVisible, psScrollY);
}
- return Status;
+ return numSpaces;
}
// Routine Description:
@@ -127,9 +151,8 @@ using Microsoft::Console::VirtualTerminal::StateMachine;
// - pcb - On input, number of bytes to write. On output, number of bytes written.
// - pcSpaces - On output, the number of spaces consumed by the written characters.
// - dwFlags -
-// WC_DESTRUCTIVE_BACKSPACE backspace overwrites characters.
+// WC_INTERACTIVE backspace overwrites characters, control characters are expanded (as in, to "^X")
// WC_KEEP_CURSOR_VISIBLE change window origin desirable when hit rt. edge
-// WC_PRINTABLE_CONTROL_CHARS if control characters should be expanded (as in, to "^X")
// Return Value:
// Note:
// - This routine does not process tabs and backspace properly. That code will be implemented as part of the line editing services.
@@ -142,565 +165,294 @@ using Microsoft::Console::VirtualTerminal::StateMachine;
const til::CoordType sOriginalXPosition,
const DWORD dwFlags,
_Inout_opt_ til::CoordType* const psScrollY)
+try
{
- const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
+ static constexpr wchar_t tabSpaces[8]{ L' ', L' ', L' ', L' ', L' ', L' ', L' ', L' ' };
+
auto& textBuffer = screenInfo.GetTextBuffer();
auto& cursor = textBuffer.GetCursor();
- auto CursorPosition = cursor.GetPosition();
- auto Status = STATUS_SUCCESS;
- til::CoordType XPosition;
- size_t TempNumSpaces = 0;
- const auto fUnprocessed = WI_IsFlagClear(screenInfo.OutputMode, ENABLE_PROCESSED_OUTPUT);
- const auto fWrapAtEOL = WI_IsFlagSet(screenInfo.OutputMode, ENABLE_WRAP_AT_EOL_OUTPUT);
-
- // Must not adjust cursor here. It has to stay on for many write scenarios. Consumers should call for the
- // cursor to be turned off if they want that.
-
- const auto Attributes = screenInfo.GetAttributes();
- const auto BufferSize = *pcb;
- *pcb = 0;
-
- auto lpString = pwchRealUnicode;
-
- const auto coordScreenBufferSize = screenInfo.GetBufferSize().Dimensions();
-
- static constexpr til::CoordType LOCAL_BUFFER_SIZE = 1024;
- WCHAR LocalBuffer[LOCAL_BUFFER_SIZE];
-
- while (*pcb < BufferSize)
+ const auto keepCursorVisible = WI_IsFlagSet(dwFlags, WC_KEEP_CURSOR_VISIBLE);
+ const auto wrapAtEOL = WI_IsFlagSet(screenInfo.OutputMode, ENABLE_WRAP_AT_EOL_OUTPUT);
+ auto it = pwchRealUnicode;
+ const auto end = it + *pcb / sizeof(wchar_t);
+ size_t numSpaces = 0;
+
+ // In VT mode, when you have a 120-column terminal you can write 120 columns without the cursor wrapping.
+ // Whenever the cursor is in that 120th column IsDelayedEOLWrap() will return true. I'm not sure why the VT parts
+ // of the code base store this as a boolean. It's also unclear why we handle this here. The intention is likely
+ // so that when we exit VT mode and receive a write a potentially stored delayed wrap would still be handled.
+ // The way this code does it however isn't correct since it handles it like the old console APIs would and
+ // so writing a newline while being delay wrapped will print 2 newlines.
+ if (cursor.IsDelayedEOLWrap() && wrapAtEOL)
{
- // correct for delayed EOL
- if (cursor.IsDelayedEOLWrap() && fWrapAtEOL)
+ auto pos = cursor.GetPosition();
+ const auto delayed = cursor.GetDelayedAtPosition();
+ cursor.ResetDelayEOLWrap();
+ if (delayed == pos)
{
- const auto coordDelayedAt = cursor.GetDelayedAtPosition();
- cursor.ResetDelayEOLWrap();
- // Only act on a delayed EOL if we didn't move the cursor to a different position from where the EOL was marked.
- if (coordDelayedAt.x == CursorPosition.x && coordDelayedAt.y == CursorPosition.y)
- {
- CursorPosition.x = 0;
- CursorPosition.y++;
+ pos.x = 0;
+ pos.y++;
+ AdjustCursorPosition(screenInfo, pos, keepCursorVisible, psScrollY);
+ }
+ }
- Status = AdjustCursorPosition(screenInfo, CursorPosition, WI_IsFlagSet(dwFlags, WC_KEEP_CURSOR_VISIBLE), psScrollY);
+ // If ENABLE_PROCESSED_OUTPUT is set we search for C0 control characters and handle them like backspace, tab, etc.
+ // If it's not set, we can just straight up give everything to _writeCharsLegacyUnprocessed.
+ if (WI_IsFlagClear(screenInfo.OutputMode, ENABLE_PROCESSED_OUTPUT))
+ {
+ numSpaces += _writeCharsLegacyUnprocessed(screenInfo, dwFlags, psScrollY, { it, end });
+ it = end;
+ }
- CursorPosition = cursor.GetPosition();
- }
+ while (it != end)
+ {
+ const auto nextControlChar = std::find_if(it, end, [](const auto& wch) { return !IS_GLYPH_CHAR(wch); });
+ if (nextControlChar != it)
+ {
+ numSpaces += _writeCharsLegacyUnprocessed(screenInfo, dwFlags, psScrollY, { it, nextControlChar });
+ it = nextControlChar;
}
- // As an optimization, collect characters in buffer and print out all at once.
- XPosition = cursor.GetPosition().x;
- til::CoordType i = 0;
- auto LocalBufPtr = LocalBuffer;
- while (*pcb < BufferSize && i < LOCAL_BUFFER_SIZE && XPosition < coordScreenBufferSize.width)
+ for (; it != end && !IS_GLYPH_CHAR(*it); ++it)
{
-#pragma prefast(suppress : 26019, "Buffer is taken in multiples of 2. Validation is ok.")
- const auto Char = *lpString;
- // WCL-NOTE: We believe RealUnicodeChar to be identical to Char, because we believe pwchRealUnicode
- // WCL-NOTE: to be identical to lpString. They are incremented in lockstep, never separately, and lpString
- // WCL-NOTE: is initialized from pwchRealUnicode.
- const auto RealUnicodeChar = *pwchRealUnicode;
- if (IS_GLYPH_CHAR(RealUnicodeChar) || fUnprocessed)
+ switch (*it)
{
- // WCL-NOTE: This operates on a single code unit instead of a whole codepoint. It will mis-measure surrogate pairs.
- if (IsGlyphFullWidth(Char))
+ case UNICODE_NULL:
+ if (WI_IsFlagSet(dwFlags, WC_INTERACTIVE))
{
- if (i < (LOCAL_BUFFER_SIZE - 1) && XPosition < (coordScreenBufferSize.width - 1))
- {
- *LocalBufPtr++ = Char;
-
- // cursor adjusted by 2 because the char is double width
- XPosition += 2;
- i += 1;
- pwchBuffer++;
- }
- else
- {
- goto EndWhile;
- }
+ break;
}
- else
+ numSpaces += _writeCharsLegacyUnprocessed(screenInfo, dwFlags, psScrollY, { &tabSpaces[0], 1 });
+ continue;
+ case UNICODE_BELL:
+ if (WI_IsFlagSet(dwFlags, WC_INTERACTIVE))
{
- *LocalBufPtr = Char;
- LocalBufPtr++;
- XPosition++;
- i++;
- pwchBuffer++;
+ break;
}
- }
- else
+ std::ignore = screenInfo.SendNotifyBeep();
+ continue;
+ case UNICODE_BACKSPACE:
{
- FAIL_FAST_IF(!(WI_IsFlagSet(screenInfo.OutputMode, ENABLE_PROCESSED_OUTPUT)));
- switch (RealUnicodeChar)
- {
- case UNICODE_BELL:
- if (dwFlags & WC_PRINTABLE_CONTROL_CHARS)
- {
- goto CtrlChar;
- }
- else
- {
- screenInfo.SendNotifyBeep();
- }
- break;
- case UNICODE_BACKSPACE:
-
- // automatically go to EndWhile. this is because
- // backspace is not destructive, so "aBkSp" prints
- // a with the cursor on the "a". we could achieve
- // this behavior staying in this loop and figuring out
- // the string that needs to be printed, but it would
- // be expensive and it's the exceptional case.
+ auto pos = cursor.GetPosition();
- goto EndWhile;
- break;
- case UNICODE_TAB:
+ if (WI_IsFlagClear(dwFlags, WC_INTERACTIVE))
{
- const auto TabSize = NUMBER_OF_SPACES_IN_TAB(XPosition);
- XPosition = XPosition + TabSize;
- if (XPosition >= coordScreenBufferSize.width)
- {
- goto EndWhile;
- }
+ pos.x = textBuffer.GetRowByOffset(pos.y).NavigateToPrevious(pos.x);
+ AdjustCursorPosition(screenInfo, pos, keepCursorVisible, psScrollY);
+ continue;
+ }
- for (til::CoordType j = 0; j < TabSize && i < LOCAL_BUFFER_SIZE; j++, i++)
- {
- *LocalBufPtr = UNICODE_SPACE;
- LocalBufPtr++;
- }
+ const auto moveUp = [&]() {
+ pos.x = -1;
+ AdjustCursorPosition(screenInfo, pos, keepCursorVisible, psScrollY);
- pwchBuffer++;
- break;
- }
- case UNICODE_LINEFEED:
- case UNICODE_CARRIAGERETURN:
- goto EndWhile;
- default:
+ const auto y = cursor.GetPosition().y;
+ auto& row = textBuffer.GetRowByOffset(y);
+
+ pos.x = textBuffer.GetSize().RightExclusive();
+ pos.y = y;
- // if char is ctrl char, write ^char.
- if ((dwFlags & WC_PRINTABLE_CONTROL_CHARS) && (IS_CONTROL_CHAR(RealUnicodeChar)))
+ if (row.WasDoubleBytePadded())
{
- CtrlChar:
- if (i < (LOCAL_BUFFER_SIZE - 1))
- {
- // WCL-NOTE: We do not properly measure that there is space for two characters
- // WCL-NOTE: left on the screen.
- *LocalBufPtr = (WCHAR)'^';
- LocalBufPtr++;
- XPosition++;
- i++;
-
- *LocalBufPtr = (WCHAR)(RealUnicodeChar + (WCHAR)'@');
- LocalBufPtr++;
- XPosition++;
- i++;
-
- pwchBuffer++;
- }
- else
- {
- goto EndWhile;
- }
+ pos.x--;
+ numSpaces--;
}
- else
- {
- if (Char == UNICODE_NULL)
- {
- *LocalBufPtr = UNICODE_SPACE;
- }
- else
- {
- // As a special favor to incompetent apps that attempt to display control chars,
- // convert to corresponding OEM Glyph Chars
- WORD CharType;
- GetStringTypeW(CT_CTYPE1, &RealUnicodeChar, 1, &CharType);
- if (WI_IsFlagSet(CharType, C1_CNTRL))
- {
- ConvertOutputToUnicode(gci.OutputCP,
- (LPSTR)&RealUnicodeChar,
- 1,
- LocalBufPtr,
- 1);
- }
- else
- {
- // WCL-NOTE: We should never hit this.
- // WCL-NOTE: 1. Normal characters are handled via the early check for IS_GLYPH_CHAR
- // WCL-NOTE: 2. Control characters are handled via the CtrlChar label (if WC_PRINTABLE_CONTROL_CHARS is on)
- // WCL-NOTE: And if they are control characters they will trigger the C1_CNTRL check above.
- *LocalBufPtr = Char;
- }
- }
+ row.SetWrapForced(false);
+ row.SetDoubleBytePadded(false);
+ };
- LocalBufPtr++;
- XPosition++;
- i++;
- pwchBuffer++;
- }
+ // We have to move up early because the tab handling code below needs to be on
+ // the row of the tab already, so that we can call GetText() for precedingText.
+ if (pos.x == 0 && pos.y != 0)
+ {
+ moveUp();
}
- }
- lpString++;
- pwchRealUnicode++;
- *pcb += sizeof(WCHAR);
- }
- EndWhile:
- if (i != 0)
- {
- CursorPosition = cursor.GetPosition();
- // Make sure we don't write past the end of the buffer.
- // WCL-NOTE: This check uses a code unit count instead of a column count. That is incorrect.
- if (i > coordScreenBufferSize.width - CursorPosition.x)
- {
- i = coordScreenBufferSize.width - CursorPosition.x;
- }
+ til::CoordType glyphCount = 1;
- // line was wrapped if we're writing up to the end of the current row
- OutputCellIterator it(std::wstring_view(LocalBuffer, i), Attributes);
- const auto itEnd = screenInfo.Write(it);
-
- // Notify accessibility
- if (screenInfo.HasAccessibilityEventing())
- {
- screenInfo.NotifyAccessibilityEventing(CursorPosition.x, CursorPosition.y, CursorPosition.x + i - 1, CursorPosition.y);
- }
-
- // The number of "spaces" or "cells" we have consumed needs to be reported and stored for later
- // when/if we need to erase the command line.
- TempNumSpaces += itEnd.GetCellDistance(it);
- // WCL-NOTE: We are using the "estimated" X position delta instead of the actual delta from
- // WCL-NOTE: the iterator. It is not clear why. If they differ, the cursor ends up in the
- // WCL-NOTE: wrong place (typically inside another character).
- CursorPosition.x = XPosition;
+ if (pwchBuffer != pwchBufferBackupLimit)
+ {
+ const auto lastChar = pwchBuffer[-1];
- Status = AdjustCursorPosition(screenInfo, CursorPosition, WI_IsFlagSet(dwFlags, WC_KEEP_CURSOR_VISIBLE), psScrollY);
+ // Deleting tabs is a bit tricky, because they have a variable width between 1 and 8 spaces,
+ // are stored as whitespace but are technically distinct from whitespace.
+ if (lastChar == UNICODE_TAB)
+ {
+ const auto precedingText = textBuffer.GetRowByOffset(pos.y).GetText(pos.x - 8, pos.x);
- // WCL-NOTE: If we have processed the entire input string during our "fast one-line print" handler,
- // WCL-NOTE: we are done as there is nothing more to do. Neat!
- if (*pcb == BufferSize)
- {
- if (nullptr != pcSpaces)
- {
- *pcSpaces = TempNumSpaces;
- }
- return STATUS_SUCCESS;
- }
- continue;
- }
- else if (*pcb >= BufferSize)
- {
- // WCL-NOTE: This case looks like it is never encountered, but it *is* if WC_PRINTABLE_CONTROL_CHARS is off.
- // WCL-NOTE: If the string is entirely nonprinting control characters, there will be
- // WCL-NOTE: no output in the buffer (LocalBuffer; i == 0) but we will have processed
- // WCL-NOTE: "every" character. We can just bail out and report the number of spaces consumed.
- FAIL_FAST_IF(!(WI_IsFlagSet(screenInfo.OutputMode, ENABLE_PROCESSED_OUTPUT)));
-
- // this catches the case where the number of backspaces == the number of characters.
- if (nullptr != pcSpaces)
- {
- *pcSpaces = TempNumSpaces;
- }
- return STATUS_SUCCESS;
- }
+ // First, we measure the amount of spaces that precede the cursor in the text buffer,
+ // which is generally the amount of spaces that we end up deleting. We do it this way,
+ // because we don't know what kind of complex mix of wide/narrow glyphs precede the tab.
+ // Basically, by asking the text buffer we get the size information of the preceding text.
+ if (precedingText.size() >= 2 && precedingText.back() == L' ')
+ {
+ auto textIt = precedingText.rbegin() + 1;
+ const auto textEnd = precedingText.rend();
- switch (*lpString)
- {
- case UNICODE_BACKSPACE:
- {
- // move cursor backwards one space. overwrite current char with blank.
- // we get here because we have to backspace from the beginning of the line
- TempNumSpaces -= 1;
- if (pwchBuffer == pwchBufferBackupLimit)
- {
- CursorPosition.x -= 1;
- }
- else
- {
- const wchar_t* Tmp;
- wchar_t* Tmp2 = nullptr;
- WCHAR LastChar;
+ for (; textIt != textEnd && *textIt == L' '; ++textIt)
+ {
+ glyphCount++;
+ }
+ }
- const auto bufferSize = pwchBuffer - pwchBufferBackupLimit;
- std::unique_ptr buffer;
- try
- {
- buffer = std::make_unique(bufferSize);
- std::fill_n(buffer.get(), bufferSize, UNICODE_NULL);
- }
- catch (...)
- {
- return NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException());
- }
+ // But there's a problem: When you print " \t" it should delete 6 spaces and not 8.
+ // In other words, we shouldn't delete any actual preceding whitespaces. We can ask
+ // the "backup" buffer (= preceding text in the commandline) for this information.
+ //
+ // backupEnd points to the character immediately preceding the tab (LastChar).
+ const auto backupEnd = pwchBuffer - 1;
+ // backupLimit points to how far back we need to search. Even if we have 9000 characters in our command line,
+ // we'll only need to check a total of 8 whitespaces. "pwchBuffer - pwchBufferBackupLimit" will
+ // always be at least 1 because that's the \t character in the backup buffer. In other words,
+ // backupLimit will at a minimum be equal to backupEnd, or precede it by 7 more characters.
+ const auto backupLimit = pwchBuffer - std::min(8, pwchBuffer - pwchBufferBackupLimit);
+ // Now count how many spaces precede the \t character. "backupEnd - backupBeg" will be the amount.
+ auto backupBeg = backupEnd;
+ for (; backupBeg != backupLimit && backupBeg[-1] == L' '; --backupBeg, --glyphCount)
+ {
+ }
- for (i = 0, Tmp2 = buffer.get(), Tmp = pwchBufferBackupLimit;
- i < bufferSize;
- i++, Tmp++)
- {
- // see 18120085, these two need to be separate if statements
- if (*Tmp == UNICODE_BACKSPACE)
- {
- //it is important we do nothing in the else case for
- // this one instead of falling through to the below else.
- if (Tmp2 > buffer.get())
+ // There's one final problem: A prompt like...
+ // fputs("foo: ", stdout);
+ // fgets(buffer, stdin);
+ // ...has a trailing whitespace in front of our pwchBufferBackupLimit which we should not backspace over.
+ // sOriginalXPosition stores the start of the prompt at the pwchBufferBackupLimit.
+ if (backupBeg == pwchBufferBackupLimit)
{
- Tmp2--;
+ glyphCount = pos.x - sOriginalXPosition;
}
+
+ // Now that we finally know how many columns precede the cursor we can
+ // subtract the previously determined amount of ' ' from the '\t'.
+ glyphCount -= gsl::narrow_cast(backupEnd - backupBeg);
+
+ // Can the above code leave glyphCount <= 0? Let's just not find out!
+ glyphCount = std::max(1, glyphCount);
}
- else
+ // Control chars in interactive mode were previously written out
+ // as ^X for instance, so now we also need to delete 2 glyphs.
+ else if (IS_CONTROL_CHAR(lastChar))
{
- FAIL_FAST_IF(!(Tmp2 >= buffer.get()));
- *Tmp2++ = *Tmp;
+ glyphCount = 2;
}
}
- if (Tmp2 == buffer.get())
- {
- LastChar = UNICODE_SPACE;
- }
- else
- {
-#pragma prefast(suppress : 26001, "This is fine. Tmp2 has to have advanced or it would equal pBuffer.")
- LastChar = *(Tmp2 - 1);
- }
- if (LastChar == UNICODE_TAB)
+ for (;;)
{
- CursorPosition.x -= RetrieveNumberOfSpaces(sOriginalXPosition,
- pwchBufferBackupLimit,
- pwchBuffer - pwchBufferBackupLimit - 1);
- if (CursorPosition.x < 0)
+ // We've already moved up if the cursor was in the first column so
+ // we need to start off with overwriting the text with whitespace.
+ // It wouldn't make sense to check the cursor position again already.
{
- CursorPosition.x = (coordScreenBufferSize.width - 1) / TAB_SIZE;
- CursorPosition.x *= TAB_SIZE;
- CursorPosition.x += 1;
- CursorPosition.y -= 1;
-
- // since you just backspaced yourself back up into the previous row, unset the wrap
- // flag on the prev row if it was set
- textBuffer.GetRowByOffset(CursorPosition.y).SetWrapForced(false);
+ const auto previousColumn = pos.x;
+ pos.x = textBuffer.GetRowByOffset(pos.y).NavigateToPrevious(previousColumn);
+
+ RowWriteState state{
+ .text = { &tabSpaces[0], 8 },
+ .columnBegin = pos.x,
+ .columnLimit = previousColumn,
+ };
+ textBuffer.Write(pos.y, textBuffer.GetCurrentAttributes(), state);
+ numSpaces -= previousColumn - pos.x;
}
- }
- else if (IS_CONTROL_CHAR(LastChar))
- {
- CursorPosition.x -= 1;
- TempNumSpaces -= 1;
- // overwrite second character of ^x sequence.
- if (dwFlags & WC_DESTRUCTIVE_BACKSPACE)
+ // The cursor movement logic is a little different for the last iteration, so we exit early here.
+ glyphCount--;
+ if (glyphCount <= 0)
{
- try
- {
- screenInfo.Write(OutputCellIterator(UNICODE_SPACE, Attributes, 1), CursorPosition);
- Status = STATUS_SUCCESS;
- }
- CATCH_LOG();
+ break;
}
- CursorPosition.x -= 1;
- }
- else if (IsGlyphFullWidth(LastChar))
- {
- CursorPosition.x -= 1;
- TempNumSpaces -= 1;
-
- Status = AdjustCursorPosition(screenInfo, CursorPosition, dwFlags & WC_KEEP_CURSOR_VISIBLE, psScrollY);
- if (dwFlags & WC_DESTRUCTIVE_BACKSPACE)
+ // Otherwise, in case we need to delete 2 or more glyphs, we need to ensure we properly wrap lines back up.
+ if (pos.x == 0 && pos.y != 0)
{
- try
- {
- screenInfo.Write(OutputCellIterator(UNICODE_SPACE, Attributes, 1), CursorPosition);
- Status = STATUS_SUCCESS;
- }
- CATCH_LOG();
+ moveUp();
}
- CursorPosition.x -= 1;
+ }
+
+ // After the last iteration the cursor might now be in the first column after a line
+ // that was previously padded with a whitespace in the last column due to a wide glyph.
+ // Now that the wide glyph is presumably gone, we can move up a line.
+ if (pos.x == 0 && pos.y != 0 && textBuffer.GetRowByOffset(pos.y - 1).WasDoubleBytePadded())
+ {
+ moveUp();
}
else
{
- CursorPosition.x--;
+ AdjustCursorPosition(screenInfo, pos, keepCursorVisible, psScrollY);
}
- }
- if ((dwFlags & WC_LIMIT_BACKSPACE) && (CursorPosition.x < 0))
- {
- CursorPosition.x = 0;
- OutputDebugStringA(("CONSRV: Ignoring backspace to previous line\n"));
- }
- Status = AdjustCursorPosition(screenInfo, CursorPosition, (dwFlags & WC_KEEP_CURSOR_VISIBLE) != 0, psScrollY);
- if (dwFlags & WC_DESTRUCTIVE_BACKSPACE)
- {
- try
+
+ // Notify accessibility to read the backspaced character.
+ // See GH:12735, MSFT:31748387
+ if (screenInfo.HasAccessibilityEventing())
{
- screenInfo.Write(OutputCellIterator(UNICODE_SPACE, Attributes, 1), cursor.GetPosition());
+ if (const auto pConsoleWindow = ServiceLocator::LocateConsoleWindow())
+ {
+ LOG_IF_FAILED(pConsoleWindow->SignalUia(UIA_Text_TextChangedEventId));
+ }
}
- CATCH_LOG();
+ continue;
}
- if (cursor.GetPosition().x == 0 && fWrapAtEOL && pwchBuffer > pwchBufferBackupLimit)
+ case UNICODE_TAB:
{
- if (CheckBisectProcessW(screenInfo,
- pwchBufferBackupLimit,
- pwchBuffer + 1 - pwchBufferBackupLimit,
- gsl::narrow_cast(coordScreenBufferSize.width) - sOriginalXPosition,
- sOriginalXPosition,
- dwFlags & WC_PRINTABLE_CONTROL_CHARS))
- {
- CursorPosition.x = coordScreenBufferSize.width - 1;
- CursorPosition.y = cursor.GetPosition().y - 1;
-
- // since you just backspaced yourself back up into the previous row, unset the wrap flag
- // on the prev row if it was set
- textBuffer.GetRowByOffset(CursorPosition.y).SetWrapForced(false);
-
- Status = AdjustCursorPosition(screenInfo, CursorPosition, dwFlags & WC_KEEP_CURSOR_VISIBLE, psScrollY);
- }
+ const auto pos = cursor.GetPosition();
+ const auto tabCount = gsl::narrow_cast(NUMBER_OF_SPACES_IN_TAB(pos.x));
+ numSpaces += _writeCharsLegacyUnprocessed(screenInfo, dwFlags, psScrollY, { &tabSpaces[0], tabCount });
+ continue;
}
- // Notify accessibility to read the backspaced character.
- // See GH:12735, MSFT:31748387
- if (screenInfo.HasAccessibilityEventing())
+ case UNICODE_LINEFEED:
{
- if (auto pConsoleWindow = ServiceLocator::LocateConsoleWindow())
+ auto pos = cursor.GetPosition();
+ if (WI_IsFlagClear(screenInfo.OutputMode, DISABLE_NEWLINE_AUTO_RETURN))
{
- LOG_IF_FAILED(pConsoleWindow->SignalUia(UIA_Text_TextChangedEventId));
+ pos.x = 0;
}
- }
- break;
- }
- case UNICODE_TAB:
- {
- const auto TabSize = NUMBER_OF_SPACES_IN_TAB(cursor.GetPosition().x);
- CursorPosition.x = cursor.GetPosition().x + TabSize;
-
- // move cursor forward to next tab stop. fill space with blanks.
- // we get here when the tab extends beyond the right edge of the
- // window. if the tab goes wraps the line, set the cursor to the first
- // position in the next line.
- pwchBuffer++;
-
- TempNumSpaces += TabSize;
- size_t NumChars = 0;
- if (CursorPosition.x >= coordScreenBufferSize.width)
- {
- NumChars = gsl::narrow(coordScreenBufferSize.width - cursor.GetPosition().x);
- CursorPosition.x = 0;
- CursorPosition.y = cursor.GetPosition().y + 1;
- // since you just tabbed yourself past the end of the row, set the wrap
- textBuffer.GetRowByOffset(cursor.GetPosition().y).SetWrapForced(true);
- }
- else
- {
- NumChars = gsl::narrow(CursorPosition.x - cursor.GetPosition().x);
- CursorPosition.y = cursor.GetPosition().y;
+ textBuffer.GetRowByOffset(pos.y).SetWrapForced(false);
+ pos.y = pos.y + 1;
+ AdjustCursorPosition(screenInfo, pos, keepCursorVisible, psScrollY);
+ continue;
}
-
- try
+ case UNICODE_CARRIAGERETURN:
{
- const OutputCellIterator it(UNICODE_SPACE, Attributes, NumChars);
- const auto done = screenInfo.Write(it, cursor.GetPosition());
- NumChars = done.GetCellDistance(it);
+ auto pos = cursor.GetPosition();
+ pos.x = 0;
+ AdjustCursorPosition(screenInfo, pos, keepCursorVisible, psScrollY);
+ continue;
}
- CATCH_LOG();
-
- Status = AdjustCursorPosition(screenInfo, CursorPosition, (dwFlags & WC_KEEP_CURSOR_VISIBLE) != 0, psScrollY);
- break;
- }
- case UNICODE_CARRIAGERETURN:
- {
- // Carriage return moves the cursor to the beginning of the line.
- // We don't need to worry about handling cr or lf for
- // backspace because input is sent to the user on cr or lf.
- pwchBuffer++;
- CursorPosition.x = 0;
- CursorPosition.y = cursor.GetPosition().y;
- Status = AdjustCursorPosition(screenInfo, CursorPosition, (dwFlags & WC_KEEP_CURSOR_VISIBLE) != 0, psScrollY);
- break;
- }
- case UNICODE_LINEFEED:
- {
- // move cursor to the next line.
- pwchBuffer++;
-
- if (WI_IsFlagClear(screenInfo.OutputMode, DISABLE_NEWLINE_AUTO_RETURN))
- {
- // Traditionally, we reset the X position to 0 with a newline automatically.
- // Some things might not want this automatic "ONLCR line discipline" (for example, things that are expecting a *NIX behavior.)
- // They will turn it off with an output mode flag.
- CursorPosition.x = 0;
+ default:
+ break;
}
- CursorPosition.y = cursor.GetPosition().y + 1;
-
+ if (WI_IsFlagSet(dwFlags, WC_INTERACTIVE) && IS_CONTROL_CHAR(*it))
{
- // since we explicitly just moved down a row, clear the wrap status on the row we just came from
- textBuffer.GetRowByOffset(cursor.GetPosition().y).SetWrapForced(false);
+ const wchar_t wchs[2]{ L'^', static_cast(*it + L'@') };
+ numSpaces += _writeCharsLegacyUnprocessed(screenInfo, dwFlags, psScrollY, { &wchs[0], 2 });
}
-
- Status = AdjustCursorPosition(screenInfo, CursorPosition, (dwFlags & WC_KEEP_CURSOR_VISIBLE) != 0, psScrollY);
- break;
- }
- default:
- {
- const auto Char = *lpString;
- if (Char >= UNICODE_SPACE &&
- IsGlyphFullWidth(Char) &&
- XPosition >= (coordScreenBufferSize.width - 1) &&
- fWrapAtEOL)
+ else
{
- const auto TargetPoint = cursor.GetPosition();
- auto& Row = textBuffer.GetRowByOffset(TargetPoint.y);
-
- try
- {
- // If we're on top of a trailing cell, clear it and the previous cell.
- if (Row.DbcsAttrAt(TargetPoint.x) == DbcsAttribute::Trailing)
- {
- // Space to clear for 2 cells.
- OutputCellIterator it(UNICODE_SPACE, 2);
-
- // Back target point up one.
- auto writeTarget = TargetPoint;
- writeTarget.x--;
-
- // Write 2 clear cells.
- screenInfo.Write(it, writeTarget);
- }
- }
- catch (...)
+ // As a special favor to incompetent apps that attempt to display control chars,
+ // convert to corresponding OEM Glyph Chars
+ const auto cp = ServiceLocator::LocateGlobals().getConsoleInformation().OutputCP;
+ const auto ch = gsl::narrow_cast(*it);
+ wchar_t wch = 0;
+ const auto result = MultiByteToWideChar(cp, MB_USEGLYPHCHARS, &ch, 1, &wch, 1);
+ if (result == 1)
{
- return NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException());
+ numSpaces += _writeCharsLegacyUnprocessed(screenInfo, dwFlags, psScrollY, { &wch, 1 });
}
-
- CursorPosition.x = 0;
- CursorPosition.y = TargetPoint.y + 1;
-
- // since you just moved yourself down onto the next row with 1 character, that sounds like a
- // forced wrap so set the flag
- Row.SetWrapForced(true);
-
- // Additionally, this padding is only called for IsConsoleFullWidth (a.k.a. when a character
- // is too wide to fit on the current line).
- Row.SetDoubleBytePadded(true);
-
- Status = AdjustCursorPosition(screenInfo, CursorPosition, dwFlags & WC_KEEP_CURSOR_VISIBLE, psScrollY);
- continue;
}
- break;
}
- }
- if (FAILED_NTSTATUS(Status))
- {
- return Status;
- }
-
- *pcb += sizeof(WCHAR);
- lpString++;
- pwchRealUnicode++;
}
- if (nullptr != pcSpaces)
+ if (pcSpaces)
{
- *pcSpaces = TempNumSpaces;
+ *pcSpaces = numSpaces;
}
- return STATUS_SUCCESS;
+ return S_OK;
}
+NT_CATCH_RETURN()
// Routine Description:
// - This routine writes a string to the screen, processing any embedded
@@ -715,9 +467,8 @@ using Microsoft::Console::VirtualTerminal::StateMachine;
// - pcb - On input, number of bytes to write. On output, number of bytes written.
// - pcSpaces - On output, the number of spaces consumed by the written characters.
// - dwFlags -
-// WC_DESTRUCTIVE_BACKSPACE backspace overwrites characters.
+// WC_INTERACTIVE backspace overwrites characters, control characters are expanded (as in, to "^X")
// WC_KEEP_CURSOR_VISIBLE change window origin (viewport) desirable when hit rt. edge
-// WC_PRINTABLE_CONTROL_CHARS if control characters should be expanded (as in, to "^X")
// Return Value:
// Note:
// - This routine does not process tabs and backspace properly. That code will be implemented as part of the line editing services.
@@ -730,9 +481,9 @@ using Microsoft::Console::VirtualTerminal::StateMachine;
const til::CoordType sOriginalXPosition,
const DWORD dwFlags,
_Inout_opt_ til::CoordType* const psScrollY)
+try
{
- if (!WI_IsFlagSet(screenInfo.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING) ||
- !WI_IsFlagSet(screenInfo.OutputMode, ENABLE_PROCESSED_OUTPUT))
+ if (WI_IsAnyFlagClear(screenInfo.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING | ENABLE_PROCESSED_OUTPUT))
{
return WriteCharsLegacy(screenInfo,
pwchBufferBackupLimit,
@@ -745,40 +496,19 @@ using Microsoft::Console::VirtualTerminal::StateMachine;
psScrollY);
}
- auto Status = STATUS_SUCCESS;
+ auto& machine = screenInfo.GetStateMachine();
+ const auto cch = *pcb / sizeof(WCHAR);
- const auto BufferSize = *pcb;
- *pcb = 0;
+ machine.ProcessString({ pwchRealUnicode, cch });
+ if (nullptr != pcSpaces)
{
- size_t TempNumSpaces = 0;
-
- {
- if (SUCCEEDED_NTSTATUS(Status))
- {
- FAIL_FAST_IF(!(WI_IsFlagSet(screenInfo.OutputMode, ENABLE_PROCESSED_OUTPUT)));
- FAIL_FAST_IF(!(WI_IsFlagSet(screenInfo.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING)));
-
- // defined down in the WriteBuffer default case hiding on the other end of the state machine. See outputStream.cpp
- // This is the only mode used by DoWriteConsole.
- FAIL_FAST_IF(!(WI_IsFlagSet(dwFlags, WC_LIMIT_BACKSPACE)));
-
- auto& machine = screenInfo.GetStateMachine();
- const auto cch = BufferSize / sizeof(WCHAR);
-
- machine.ProcessString({ pwchRealUnicode, cch });
- *pcb += BufferSize;
- }
- }
-
- if (nullptr != pcSpaces)
- {
- *pcSpaces = TempNumSpaces;
- }
+ *pcSpaces = 0;
}
- return Status;
+ return STATUS_SUCCESS;
}
+NT_CATCH_RETURN()
// Routine Description:
// - Takes the given text and inserts it into the given screen buffer.
@@ -841,7 +571,7 @@ using Microsoft::Console::VirtualTerminal::StateMachine;
pcbBuffer,
nullptr,
textBuffer.GetCursor().GetPosition().x,
- WC_LIMIT_BACKSPACE,
+ 0,
nullptr);
}
diff --git a/src/host/_stream.h b/src/host/_stream.h
index 149482f1ff1..58351c86cc1 100644
--- a/src/host/_stream.h
+++ b/src/host/_stream.h
@@ -35,10 +35,7 @@ Routine Description:
Return Value:
--*/
-[[nodiscard]] NTSTATUS AdjustCursorPosition(SCREEN_INFORMATION& screenInfo,
- _In_ til::point coordCursor,
- const BOOL fKeepCursorVisible,
- _Inout_opt_ til::CoordType* psScrollY);
+void AdjustCursorPosition(SCREEN_INFORMATION& screenInfo, _In_ til::point coordCursor, const BOOL fKeepCursorVisible, _Inout_opt_ til::CoordType* psScrollY);
/*++
Routine Description:
@@ -57,9 +54,8 @@ Routine Description:
bytes written.
NumSpaces - On output, the number of spaces consumed by the written characters.
dwFlags -
- WC_DESTRUCTIVE_BACKSPACE backspace overwrites characters.
+ WC_INTERACTIVE backspace overwrites characters, control characters are expanded (as in, to "^X")
WC_KEEP_CURSOR_VISIBLE change window origin desirable when hit rt. edge
- WC_PRINTABLE_CONTROL_CHARS if control characters should be expanded (as in, to "^X")
Return Value:
diff --git a/src/host/cmdline.cpp b/src/host/cmdline.cpp
index 1675962144b..4b48f4bd24b 100644
--- a/src/host/cmdline.cpp
+++ b/src/host/cmdline.cpp
@@ -251,7 +251,7 @@ void RedrawCommandLine(COOKED_READ_DATA& cookedReadData)
&cookedReadData.BytesRead(),
&cookedReadData.VisibleCharCount(),
cookedReadData.OriginalCursorPosition().x,
- WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
+ WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE,
&ScrollY);
FAIL_FAST_IF_NTSTATUS_FAILED(Status);
@@ -268,8 +268,7 @@ void RedrawCommandLine(COOKED_READ_DATA& cookedReadData)
{
CursorPosition.x++;
}
- Status = AdjustCursorPosition(cookedReadData.ScreenInfo(), CursorPosition, TRUE, nullptr);
- FAIL_FAST_IF_NTSTATUS_FAILED(Status);
+ AdjustCursorPosition(cookedReadData.ScreenInfo(), CursorPosition, TRUE, nullptr);
}
}
@@ -292,7 +291,7 @@ void SetCurrentCommandLine(COOKED_READ_DATA& cookedReadData, _In_ SHORT Index) /
&cookedReadData.BytesRead(),
&cookedReadData.VisibleCharCount(),
cookedReadData.OriginalCursorPosition().x,
- WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
+ WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE,
&ScrollY));
cookedReadData.OriginalCursorPosition().y += ScrollY;
}
@@ -456,7 +455,7 @@ void CommandLine::_processHistoryCycling(COOKED_READ_DATA& cookedReadData,
&cookedReadData.BytesRead(),
&cookedReadData.VisibleCharCount(),
cookedReadData.OriginalCursorPosition().x,
- WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
+ WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE,
&ScrollY));
cookedReadData.OriginalCursorPosition().y += ScrollY;
}
@@ -491,7 +490,7 @@ void CommandLine::_setPromptToOldestCommand(COOKED_READ_DATA& cookedReadData)
&cookedReadData.BytesRead(),
&cookedReadData.VisibleCharCount(),
cookedReadData.OriginalCursorPosition().x,
- WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
+ WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE,
&ScrollY));
cookedReadData.OriginalCursorPosition().y += ScrollY;
}
@@ -527,7 +526,7 @@ void CommandLine::_setPromptToNewestCommand(COOKED_READ_DATA& cookedReadData)
&cookedReadData.BytesRead(),
&cookedReadData.VisibleCharCount(),
cookedReadData.OriginalCursorPosition().x,
- WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
+ WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE,
&ScrollY));
cookedReadData.OriginalCursorPosition().y += ScrollY;
}
@@ -554,7 +553,7 @@ void CommandLine::DeletePromptAfterCursor(COOKED_READ_DATA& cookedReadData) noex
&cookedReadData.BytesRead(),
&cookedReadData.VisibleCharCount(),
cookedReadData.OriginalCursorPosition().x,
- WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
+ WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE,
nullptr));
}
}
@@ -581,7 +580,7 @@ til::point CommandLine::_deletePromptBeforeCursor(COOKED_READ_DATA& cookedReadDa
&cookedReadData.BytesRead(),
&cookedReadData.VisibleCharCount(),
cookedReadData.OriginalCursorPosition().x,
- WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
+ WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE,
nullptr));
}
return cookedReadData.OriginalCursorPosition();
@@ -870,7 +869,7 @@ til::point CommandLine::_moveCursorRight(COOKED_READ_DATA& cookedReadData) noexc
&CharsToWrite,
&NumSpaces,
cookedReadData.OriginalCursorPosition().x,
- WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
+ WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE,
&ScrollY));
cookedReadData.OriginalCursorPosition().y += ScrollY;
cookedReadData.VisibleCharCount() += NumSpaces;
@@ -913,7 +912,7 @@ void CommandLine::_insertCtrlZ(COOKED_READ_DATA& cookedReadData) noexcept
&CharsToWrite,
&NumSpaces,
cookedReadData.OriginalCursorPosition().x,
- WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
+ WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE,
&ScrollY));
cookedReadData.OriginalCursorPosition().y += ScrollY;
cookedReadData.VisibleCharCount() += NumSpaces;
@@ -963,7 +962,7 @@ void CommandLine::_fillPromptWithPreviousCommandFragment(COOKED_READ_DATA& cooke
&cchCount,
&NumSpaces,
cookedReadData.OriginalCursorPosition().x,
- WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
+ WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE,
&ScrollY));
cookedReadData.OriginalCursorPosition().y += ScrollY;
cookedReadData.VisibleCharCount() += NumSpaces;
@@ -1008,7 +1007,7 @@ til::point CommandLine::_cycleMatchingCommandHistoryToPrompt(COOKED_READ_DATA& c
&cookedReadData.BytesRead(),
&cookedReadData.VisibleCharCount(),
cookedReadData.OriginalCursorPosition().x,
- WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
+ WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE,
&ScrollY));
cookedReadData.OriginalCursorPosition().y += ScrollY;
cursorPosition.y += ScrollY;
@@ -1063,7 +1062,7 @@ til::point CommandLine::DeleteFromRightOfCursor(COOKED_READ_DATA& cookedReadData
&cookedReadData.BytesRead(),
&cookedReadData.VisibleCharCount(),
cookedReadData.OriginalCursorPosition().x,
- WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
+ WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE,
nullptr));
}
@@ -1298,8 +1297,7 @@ til::point CommandLine::DeleteFromRightOfCursor(COOKED_READ_DATA& cookedReadData
if (UpdateCursorPosition && cookedReadData.IsEchoInput())
{
- Status = AdjustCursorPosition(cookedReadData.ScreenInfo(), cursorPosition, true, nullptr);
- FAIL_FAST_IF_NTSTATUS_FAILED(Status);
+ AdjustCursorPosition(cookedReadData.ScreenInfo(), cursorPosition, true, nullptr);
}
return STATUS_SUCCESS;
diff --git a/src/host/cmdline.h b/src/host/cmdline.h
index 32f3ee4e7b4..a8669edadde 100644
--- a/src/host/cmdline.h
+++ b/src/host/cmdline.h
@@ -131,18 +131,8 @@ void DeleteCommandLine(COOKED_READ_DATA& cookedReadData, const bool fUpdateField
void RedrawCommandLine(COOKED_READ_DATA& cookedReadData);
// Values for WriteChars(), WriteCharsLegacy() dwFlags
-#define WC_DESTRUCTIVE_BACKSPACE 0x01
+#define WC_INTERACTIVE 0x01
#define WC_KEEP_CURSOR_VISIBLE 0x02
-#define WC_PRINTABLE_CONTROL_CHARS 0x04
-
-// This is no longer necessary. The buffer will always be Unicode. We don't need to perform special work to check if we're in a raster font
-// and convert the entire buffer to match (and all insertions).
-//#define WC_FALSIFY_UNICODE 0x08
-
-#define WC_LIMIT_BACKSPACE 0x10
-//#define WC_NONDESTRUCTIVE_TAB 0x20 - This is not needed anymore, because the VT code handles tabs internally now.
-//#define WC_NEWLINE_SAVE_X 0x40 - This has been replaced with an output mode flag instead as it's line discipline behavior that may not necessarily be coupled with VT.
-//#define WC_DELAY_EOL_WRAP 0x80 - This is not needed anymore, because the AdaptDispatch class handles all VT output.
// Word delimiters
bool IsWordDelim(const wchar_t wch);
diff --git a/src/host/directio.cpp b/src/host/directio.cpp
index 96661c60b70..8e6456f0526 100644
--- a/src/host/directio.cpp
+++ b/src/host/directio.cpp
@@ -149,13 +149,13 @@ void EventsToUnicode(_Inout_ std::deque>& inEvents,
// - CONSOLE_STATUS_WAIT - If we didn't have enough data or needed to
// block, this will be returned along with context in *ppWaiter.
// - Or an out of memory/math/string error message in NTSTATUS format.
-[[nodiscard]] static NTSTATUS _DoGetConsoleInput(InputBuffer& inputBuffer,
- std::deque>& outEvents,
- const size_t eventReadCount,
- INPUT_READ_HANDLE_DATA& readHandleState,
- const bool IsUnicode,
- const bool IsPeek,
- std::unique_ptr& waiter) noexcept
+[[nodiscard]] HRESULT ApiRoutines::GetConsoleInputImpl(IConsoleInputObject& inputBuffer,
+ InputEventQueue& outEvents,
+ const size_t eventReadCount,
+ INPUT_READ_HANDLE_DATA& readHandleState,
+ const bool IsUnicode,
+ const bool IsPeek,
+ std::unique_ptr& waiter) noexcept
{
try
{
@@ -182,174 +182,10 @@ void EventsToUnicode(_Inout_ std::deque>& inEvents,
// to the read data object and send it back up to the server.
waiter = std::make_unique(&inputBuffer,
&readHandleState,
- eventReadCount,
- std::move(outEvents));
+ eventReadCount);
}
return Status;
}
- catch (...)
- {
- return NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException());
- }
-}
-
-// Routine Description:
-// - Retrieves input records from the given input object and returns them to the client.
-// - The peek version will NOT remove records when it copies them out.
-// - The A version will convert to W using the console's current Input codepage (see SetConsoleCP)
-// Arguments:
-// - context - The input buffer to take records from to return to the client
-// - outEvents - storage location for read events
-// - eventsToRead - The number of input events to read
-// - readHandleState - A structure that will help us maintain
-// some input context across various calls on the same input
-// handle. Primarily used to restore the "other piece" of partially
-// returned strings (because client buffer wasn't big enough) on the
-// next call.
-// - waiter - If we have to wait (not enough data to fill client
-// buffer), this contains context that will allow the server to
-// restore this call later.
-[[nodiscard]] HRESULT ApiRoutines::PeekConsoleInputAImpl(IConsoleInputObject& context,
- std::deque>& outEvents,
- const size_t eventsToRead,
- INPUT_READ_HANDLE_DATA& readHandleState,
- std::unique_ptr& waiter) noexcept
-{
- try
- {
- auto Status = _DoGetConsoleInput(context,
- outEvents,
- eventsToRead,
- readHandleState,
- false,
- true,
- waiter);
- if (CONSOLE_STATUS_WAIT == Status)
- {
- return HRESULT_FROM_NT(Status);
- }
- RETURN_NTSTATUS(Status);
- }
- CATCH_RETURN();
-}
-
-// Routine Description:
-// - Retrieves input records from the given input object and returns them to the client.
-// - The peek version will NOT remove records when it copies them out.
-// - The W version accepts UCS-2 formatted characters (wide characters)
-// Arguments:
-// - context - The input buffer to take records from to return to the client
-// - outEvents - storage location for read events
-// - eventsToRead - The number of input events to read
-// - readHandleState - A structure that will help us maintain
-// some input context across various calls on the same input
-// handle. Primarily used to restore the "other piece" of partially
-// returned strings (because client buffer wasn't big enough) on the
-// next call.
-// - waiter - If we have to wait (not enough data to fill client
-// buffer), this contains context that will allow the server to
-// restore this call later.
-[[nodiscard]] HRESULT ApiRoutines::PeekConsoleInputWImpl(IConsoleInputObject& context,
- std::deque>& outEvents,
- const size_t eventsToRead,
- INPUT_READ_HANDLE_DATA& readHandleState,
- std::unique_ptr& waiter) noexcept
-{
- try
- {
- auto Status = _DoGetConsoleInput(context,
- outEvents,
- eventsToRead,
- readHandleState,
- true,
- true,
- waiter);
- if (CONSOLE_STATUS_WAIT == Status)
- {
- return HRESULT_FROM_NT(Status);
- }
- RETURN_NTSTATUS(Status);
- }
- CATCH_RETURN();
-}
-
-// Routine Description:
-// - Retrieves input records from the given input object and returns them to the client.
-// - The read version WILL remove records when it copies them out.
-// - The A version will convert to W using the console's current Input codepage (see SetConsoleCP)
-// Arguments:
-// - context - The input buffer to take records from to return to the client
-// - outEvents - storage location for read events
-// - eventsToRead - The number of input events to read
-// - readHandleState - A structure that will help us maintain
-// some input context across various calls on the same input
-// handle. Primarily used to restore the "other piece" of partially
-// returned strings (because client buffer wasn't big enough) on the
-// next call.
-// - waiter - If we have to wait (not enough data to fill client
-// buffer), this contains context that will allow the server to
-// restore this call later.
-[[nodiscard]] HRESULT ApiRoutines::ReadConsoleInputAImpl(IConsoleInputObject& context,
- std::deque>& outEvents,
- const size_t eventsToRead,
- INPUT_READ_HANDLE_DATA& readHandleState,
- std::unique_ptr& waiter) noexcept
-{
- try
- {
- auto Status = _DoGetConsoleInput(context,
- outEvents,
- eventsToRead,
- readHandleState,
- false,
- false,
- waiter);
- if (CONSOLE_STATUS_WAIT == Status)
- {
- return HRESULT_FROM_NT(Status);
- }
- RETURN_NTSTATUS(Status);
- }
- CATCH_RETURN();
-}
-
-// Routine Description:
-// - Retrieves input records from the given input object and returns them to the client.
-// - The read version WILL remove records when it copies them out.
-// - The W version accepts UCS-2 formatted characters (wide characters)
-// Arguments:
-// - context - The input buffer to take records from to return to the client
-// - outEvents - storage location for read events
-// - eventsToRead - The number of input events to read
-// - readHandleState - A structure that will help us maintain
-// some input context across various calls on the same input
-// handle. Primarily used to restore the "other piece" of partially
-// returned strings (because client buffer wasn't big enough) on the
-// next call.
-// - waiter - If we have to wait (not enough data to fill client
-// buffer), this contains context that will allow the server to
-// restore this call later.
-[[nodiscard]] HRESULT ApiRoutines::ReadConsoleInputWImpl(IConsoleInputObject& context,
- std::deque>& outEvents,
- const size_t eventsToRead,
- INPUT_READ_HANDLE_DATA& readHandleState,
- std::unique_ptr& waiter) noexcept
-{
- try
- {
- auto Status = _DoGetConsoleInput(context,
- outEvents,
- eventsToRead,
- readHandleState,
- true,
- false,
- waiter);
- if (CONSOLE_STATUS_WAIT == Status)
- {
- return HRESULT_FROM_NT(Status);
- }
- RETURN_NTSTATUS(Status);
- }
CATCH_RETURN();
}
diff --git a/src/host/ft_fuzzer/fuzzmain.cpp b/src/host/ft_fuzzer/fuzzmain.cpp
index 87f2ee78897..4b8f04dea6e 100644
--- a/src/host/ft_fuzzer/fuzzmain.cpp
+++ b/src/host/ft_fuzzer/fuzzmain.cpp
@@ -142,7 +142,7 @@ extern "C" __declspec(dllexport) int LLVMFuzzerTestOneInput(const uint8_t* data,
&sizeInBytes,
nullptr,
0,
- WC_PRINTABLE_CONTROL_CHARS | WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE,
+ WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE,
&scrollY);
return 0;
}
diff --git a/src/host/input.cpp b/src/host/input.cpp
index 6d3db08ff0e..94e58d1407a 100644
--- a/src/host/input.cpp
+++ b/src/host/input.cpp
@@ -195,8 +195,7 @@ void HandleFocusEvent(const BOOL fSetFocus)
try
{
- const auto EventsWritten = gci.pInputBuffer->Write(std::make_unique(!!fSetFocus));
- FAIL_FAST_IF(EventsWritten != 1);
+ gci.pInputBuffer->WriteFocusEvent(fSetFocus);
}
catch (...)
{
diff --git a/src/host/inputBuffer.cpp b/src/host/inputBuffer.cpp
index 215b49ceb69..0b4fe825b54 100644
--- a/src/host/inputBuffer.cpp
+++ b/src/host/inputBuffer.cpp
@@ -26,13 +26,8 @@ using namespace Microsoft::Console;
// - A new instance of InputBuffer
InputBuffer::InputBuffer() :
InputMode{ INPUT_BUFFER_DEFAULT_INPUT_MODE },
- WaitQueue{},
- _pTtyConnection(nullptr),
- _termInput(std::bind(&InputBuffer::_HandleTerminalInputCallback, this, std::placeholders::_1))
+ _pTtyConnection(nullptr)
{
- // The _termInput's constructor takes a reference to this object's _HandleTerminalInputCallback.
- // We need to use std::bind to create a reference to that function without a reference to this InputBuffer
-
// initialize buffer header
fInComposition = false;
}
@@ -682,6 +677,63 @@ size_t InputBuffer::Write(_Inout_ std::deque>& inEv
}
}
+// This can be considered a "privileged" variant of Write() which allows FOCUS_EVENTs to generate focus VT sequences.
+// If we didn't do this, someone could write a FOCUS_EVENT_RECORD with WriteConsoleInput, exit without flushing the
+// input buffer and the next application will suddenly get a "\x1b[I" sequence in their input. See GH#13238.
+void InputBuffer::WriteFocusEvent(bool focused) noexcept
+{
+ if (IsInVirtualTerminalInputMode())
+ {
+ if (const auto out = _termInput.HandleFocus(focused))
+ {
+ _HandleTerminalInputCallback(*out);
+ }
+ }
+ else
+ {
+ // This is a mini-version of Write().
+ const auto wasEmpty = _storage.empty();
+ _storage.push_back(std::make_unique(focused));
+ if (wasEmpty)
+ {
+ ServiceLocator::LocateGlobals().hInputEvent.SetEvent();
+ }
+ WakeUpReadersWaitingForData();
+ }
+}
+
+// Returns true when mouse input started. You should then capture the mouse and produce further events.
+bool InputBuffer::WriteMouseEvent(til::point position, const unsigned int button, const short keyState, const short wheelDelta)
+{
+ if (IsInVirtualTerminalInputMode())
+ {
+ // This magic flag is "documented" at https://msdn.microsoft.com/en-us/library/windows/desktop/ms646301(v=vs.85).aspx
+ // "If the high-order bit is 1, the key is down; otherwise, it is up."
+ static constexpr short KeyPressed{ gsl::narrow_cast(0x8000) };
+
+ const TerminalInput::MouseButtonState state{
+ WI_IsFlagSet(OneCoreSafeGetKeyState(VK_LBUTTON), KeyPressed),
+ WI_IsFlagSet(OneCoreSafeGetKeyState(VK_MBUTTON), KeyPressed),
+ WI_IsFlagSet(OneCoreSafeGetKeyState(VK_RBUTTON), KeyPressed)
+ };
+
+ // GH#6401: VT applications should be able to receive mouse events from outside the
+ // terminal buffer. This is likely to happen when the user drags the cursor offscreen.
+ // We shouldn't throw away perfectly good events when they're offscreen, so we just
+ // clamp them to be within the range [(0, 0), (W, H)].
+ const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
+ gci.GetActiveOutputBuffer().GetViewport().ToOrigin().Clamp(position);
+
+ if (const auto out = _termInput.HandleMouse(position, button, keyState, wheelDelta, state))
+ {
+ _HandleTerminalInputCallback(*out);
+ return true;
+ }
+ }
+
+ return false;
+}
+
// Routine Description:
// - Coalesces input events and transfers them to storage queue.
// Arguments:
@@ -715,10 +767,9 @@ void InputBuffer::_WriteBuffer(_Inout_ std::deque>&
inEvents.pop_front();
if (vtInputMode)
{
- // GH#11682: TerminalInput::HandleKey can handle both KeyEvents and Focus events seamlessly
- const auto handled = _termInput.HandleKey(inEvent.get());
- if (handled)
+ if (const auto out = _termInput.HandleKey(inEvent.get()))
{
+ _HandleTerminalInputCallback(*out);
eventsWritten++;
continue;
}
@@ -944,16 +995,18 @@ bool InputBuffer::IsInVirtualTerminalInputMode() const
// - inEvents - Series of input records to insert into the buffer
// Return Value:
// -
-void InputBuffer::_HandleTerminalInputCallback(std::deque>& inEvents)
+void InputBuffer::_HandleTerminalInputCallback(const TerminalInput::StringType& text)
{
try
{
- // add all input events to the storage queue
- while (!inEvents.empty())
+ if (text.empty())
{
- auto inEvent = std::move(inEvents.front());
- inEvents.pop_front();
- _storage.push_back(std::move(inEvent));
+ return;
+ }
+
+ for (const auto& wch : text)
+ {
+ _storage.push_back(std::make_unique(true, 1ui16, 0ui16, 0ui16, wch, 0));
}
if (!_vtInputShouldSuppress)
diff --git a/src/host/inputBuffer.hpp b/src/host/inputBuffer.hpp
index f5103ea9f98..2cb122181da 100644
--- a/src/host/inputBuffer.hpp
+++ b/src/host/inputBuffer.hpp
@@ -81,6 +81,9 @@ class InputBuffer final : public ConsoleObjectHeader
size_t Write(_Inout_ std::unique_ptr inEvent);
size_t Write(_Inout_ std::deque>& inEvents);
+ void WriteFocusEvent(bool focused) noexcept;
+ bool WriteMouseEvent(til::point position, unsigned int button, short keyState, short wheelDelta);
+
bool IsInVirtualTerminalInputMode() const;
Microsoft::Console::VirtualTerminal::TerminalInput& GetTerminalInput();
void SetTerminalConnection(_In_ Microsoft::Console::Render::VtEngine* const pTtyConnection);
@@ -125,7 +128,7 @@ class InputBuffer final : public ConsoleObjectHeader
bool _CoalesceRepeatedKeyPressEvents(_Inout_ std::deque>& inEvents);
void _HandleConsoleSuspensionEvents(_Inout_ std::deque>& inEvents);
- void _HandleTerminalInputCallback(_In_ std::deque>& inEvents);
+ void _HandleTerminalInputCallback(const Microsoft::Console::VirtualTerminal::TerminalInput::StringType& text);
#ifdef UNIT_TESTING
friend class InputBufferTests;
diff --git a/src/host/output.cpp b/src/host/output.cpp
index 8b0ce0b7fd9..75f522de314 100644
--- a/src/host/output.cpp
+++ b/src/host/output.cpp
@@ -299,32 +299,30 @@ static void _ScrollScreen(SCREEN_INFORMATION& screenInfo, const Viewport& source
// - screenInfo - reference to screen buffer info.
// Return Value:
// - true if we succeeded in scrolling the buffer, otherwise false (if we're out of memory)
-bool StreamScrollRegion(SCREEN_INFORMATION& screenInfo)
+void StreamScrollRegion(SCREEN_INFORMATION& screenInfo)
{
// Rotate the circular buffer around and wipe out the previous final line.
- auto fSuccess = screenInfo.GetTextBuffer().IncrementCircularBuffer();
- if (fSuccess)
+ auto& buffer = screenInfo.GetTextBuffer();
+ buffer.IncrementCircularBuffer(buffer.GetCurrentAttributes());
+
+ // Trigger a graphical update if we're active.
+ if (screenInfo.IsActiveScreenBuffer())
{
- // Trigger a graphical update if we're active.
- if (screenInfo.IsActiveScreenBuffer())
- {
- til::point coordDelta;
- coordDelta.y = -1;
+ til::point coordDelta;
+ coordDelta.y = -1;
- auto pNotifier = ServiceLocator::LocateAccessibilityNotifier();
- if (pNotifier)
- {
- // Notify accessibility that a scroll has occurred.
- pNotifier->NotifyConsoleUpdateScrollEvent(coordDelta.x, coordDelta.y);
- }
+ auto pNotifier = ServiceLocator::LocateAccessibilityNotifier();
+ if (pNotifier)
+ {
+ // Notify accessibility that a scroll has occurred.
+ pNotifier->NotifyConsoleUpdateScrollEvent(coordDelta.x, coordDelta.y);
+ }
- if (ServiceLocator::LocateGlobals().pRender != nullptr)
- {
- ServiceLocator::LocateGlobals().pRender->TriggerScroll(&coordDelta);
- }
+ if (ServiceLocator::LocateGlobals().pRender != nullptr)
+ {
+ ServiceLocator::LocateGlobals().pRender->TriggerScroll(&coordDelta);
}
}
- return fSuccess;
}
// Routine Description:
diff --git a/src/host/output.h b/src/host/output.h
index 849ba505596..fff6c001c25 100644
--- a/src/host/output.h
+++ b/src/host/output.h
@@ -47,7 +47,7 @@ void ScrollRegion(SCREEN_INFORMATION& screenInfo,
VOID SetConsoleWindowOwner(const HWND hwnd, _Inout_opt_ ConsoleProcessHandle* pProcessData);
-bool StreamScrollRegion(SCREEN_INFORMATION& screenInfo);
+void StreamScrollRegion(SCREEN_INFORMATION& screenInfo);
// For handling process handle state, not the window state itself.
void CloseConsoleProcessState();
diff --git a/src/host/outputStream.cpp b/src/host/outputStream.cpp
index 301f73a30b1..0394e27ad6b 100644
--- a/src/host/outputStream.cpp
+++ b/src/host/outputStream.cpp
@@ -183,11 +183,13 @@ void ConhostInternalGetSet::SetWindowTitle(std::wstring_view title)
// - Swaps to the alternate screen buffer. In virtual terminals, there exists both a "main"
// screen buffer and an alternate. This creates a new alternate, and switches to it.
// If there is an already existing alternate, it is discarded.
+// Arguments:
+// - attrs - the attributes the buffer is initialized with.
// Return Value:
// -
-void ConhostInternalGetSet::UseAlternateScreenBuffer()
+void ConhostInternalGetSet::UseAlternateScreenBuffer(const TextAttribute& attrs)
{
- THROW_IF_NTSTATUS_FAILED(_io.GetActiveOutputBuffer().UseAlternateScreenBuffer());
+ THROW_IF_NTSTATUS_FAILED(_io.GetActiveOutputBuffer().UseAlternateScreenBuffer(attrs));
}
// Routine Description:
@@ -404,7 +406,7 @@ bool ConhostInternalGetSet::IsVtInputEnabled() const
void ConhostInternalGetSet::NotifyAccessibilityChange(const til::rect& changedRect)
{
auto& screenInfo = _io.GetActiveOutputBuffer();
- if (screenInfo.HasAccessibilityEventing())
+ if (screenInfo.HasAccessibilityEventing() && changedRect)
{
screenInfo.NotifyAccessibilityEventing(
changedRect.left,
diff --git a/src/host/outputStream.hpp b/src/host/outputStream.hpp
index 01a3e560e70..bf3db4d3f4c 100644
--- a/src/host/outputStream.hpp
+++ b/src/host/outputStream.hpp
@@ -45,7 +45,7 @@ class ConhostInternalGetSet final : public Microsoft::Console::VirtualTerminal::
void SetWindowTitle(const std::wstring_view title) override;
- void UseAlternateScreenBuffer() override;
+ void UseAlternateScreenBuffer(const TextAttribute& attrs) override;
void UseMainScreenBuffer() override;
diff --git a/src/host/readData.hpp b/src/host/readData.hpp
index dce1c3e1fee..b40b03b5dec 100644
--- a/src/host/readData.hpp
+++ b/src/host/readData.hpp
@@ -32,20 +32,9 @@ class ReadData : public IWaitRoutine
ReadData(_In_ InputBuffer* const pInputBuffer,
_In_ INPUT_READ_HANDLE_DATA* const pInputReadHandleData);
- virtual ~ReadData();
+ ~ReadData() override;
- ReadData(const ReadData&) = delete;
ReadData(ReadData&&);
- ReadData& operator=(const ReadData&) & = delete;
- ReadData& operator=(ReadData&&) & = delete;
-
- virtual void MigrateUserBuffersOnTransitionToBackgroundWait(const void* oldBuffer, void* newBuffer) = 0;
- virtual bool Notify(const WaitTerminationReason TerminationReason,
- const bool fIsUnicode,
- _Out_ NTSTATUS* const pReplyStatus,
- _Out_ size_t* const pNumBytes,
- _Out_ DWORD* const pControlKeyState,
- _Out_ void* const pOutputData) = 0;
InputBuffer* GetInputBuffer() const;
INPUT_READ_HANDLE_DATA* GetInputReadHandleData() const;
diff --git a/src/host/readDataCooked.cpp b/src/host/readDataCooked.cpp
index 6ea80796f7e..6da7ed899ab 100644
--- a/src/host/readDataCooked.cpp
+++ b/src/host/readDataCooked.cpp
@@ -534,7 +534,7 @@ bool COOKED_READ_DATA::ProcessInput(const wchar_t wchOrig,
&NumToWrite,
&NumSpaces,
_originalCursorPosition.x,
- WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
+ WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE,
&ScrollY);
if (SUCCEEDED_NTSTATUS(status))
{
@@ -616,7 +616,7 @@ bool COOKED_READ_DATA::ProcessInput(const wchar_t wchOrig,
&NumToWrite,
nullptr,
_originalCursorPosition.x,
- WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
+ WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE,
nullptr);
if (FAILED_NTSTATUS(status))
{
@@ -717,7 +717,7 @@ bool COOKED_READ_DATA::ProcessInput(const wchar_t wchOrig,
// write the new command line to the screen
NumToWrite = _bytesRead;
- DWORD dwFlags = WC_DESTRUCTIVE_BACKSPACE | WC_PRINTABLE_CONTROL_CHARS;
+ DWORD dwFlags = WC_INTERACTIVE;
if (wch == UNICODE_CARRIAGERETURN)
{
dwFlags |= WC_KEEP_CURSOR_VISIBLE;
@@ -757,12 +757,7 @@ bool COOKED_READ_DATA::ProcessInput(const wchar_t wchOrig,
// adjust cursor position for WriteChars
_originalCursorPosition.y += ScrollY;
CursorPosition.y += ScrollY;
- status = AdjustCursorPosition(_screenInfo, CursorPosition, TRUE, nullptr);
- if (FAILED_NTSTATUS(status))
- {
- _bytesRead = 0;
- return true;
- }
+ AdjustCursorPosition(_screenInfo, CursorPosition, TRUE, nullptr);
}
}
}
@@ -787,7 +782,7 @@ bool COOKED_READ_DATA::ProcessInput(const wchar_t wchOrig,
&NumToWrite,
nullptr,
_originalCursorPosition.x,
- WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
+ WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE,
nullptr);
if (FAILED_NTSTATUS(status))
{
@@ -849,7 +844,7 @@ size_t COOKED_READ_DATA::Write(const std::wstring_view wstr)
&bytesInserted,
&NumSpaces,
OriginalCursorPosition().x,
- WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
+ WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE,
&ScrollY));
OriginalCursorPosition().y += ScrollY;
VisibleCharCount() += NumSpaces;
diff --git a/src/host/readDataDirect.cpp b/src/host/readDataDirect.cpp
index c4f58cf3c64..9f8edc3daad 100644
--- a/src/host/readDataDirect.cpp
+++ b/src/host/readDataDirect.cpp
@@ -22,8 +22,7 @@
// - THROW: Throws E_INVALIDARG for invalid pointers.
DirectReadData::DirectReadData(_In_ InputBuffer* const pInputBuffer,
_In_ INPUT_READ_HANDLE_DATA* const pInputReadHandleData,
- const size_t eventReadCount,
- _In_ std::deque> partialEvents) :
+ const size_t eventReadCount) :
ReadData(pInputBuffer, pInputReadHandleData),
_eventReadCount{ eventReadCount }
{
diff --git a/src/host/readDataDirect.hpp b/src/host/readDataDirect.hpp
index 820ddd59819..399b8a99ca8 100644
--- a/src/host/readDataDirect.hpp
+++ b/src/host/readDataDirect.hpp
@@ -33,8 +33,7 @@ class DirectReadData final : public ReadData
public:
DirectReadData(_In_ InputBuffer* const pInputBuffer,
_In_ INPUT_READ_HANDLE_DATA* const pInputReadHandleData,
- const size_t eventReadCount,
- _In_ std::deque> partialEvents);
+ const size_t eventReadCount);
DirectReadData(DirectReadData&&) = default;
diff --git a/src/host/renderData.cpp b/src/host/renderData.cpp
index 80fbf9c8eba..1f1dadda001 100644
--- a/src/host/renderData.cpp
+++ b/src/host/renderData.cpp
@@ -246,7 +246,7 @@ const std::vector RenderData::GetOver
// -
// Return Value:
// - true if the cursor should be drawn twice as wide as usual
-bool RenderData::IsCursorDoubleWidth() const noexcept
+bool RenderData::IsCursorDoubleWidth() const
{
const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
return gci.GetActiveOutputBuffer().CursorIsDoubleWidth();
diff --git a/src/host/renderData.hpp b/src/host/renderData.hpp
index 8263f35cd70..4d796812a81 100644
--- a/src/host/renderData.hpp
+++ b/src/host/renderData.hpp
@@ -36,7 +36,7 @@ class RenderData final :
ULONG GetCursorHeight() const noexcept override;
CursorType GetCursorStyle() const noexcept override;
ULONG GetCursorPixelWidth() const noexcept override;
- bool IsCursorDoubleWidth() const noexcept override;
+ bool IsCursorDoubleWidth() const override;
const std::vector GetOverlays() const noexcept override;
@@ -54,8 +54,8 @@ class RenderData final :
const bool IsBlockSelection() const noexcept override;
void ClearSelection() override;
void SelectNewRegion(const til::point coordStart, const til::point coordEnd) override;
- const til::point GetSelectionAnchor() const noexcept;
- const til::point GetSelectionEnd() const noexcept;
- void ColorSelection(const til::point coordSelectionStart, const til::point coordSelectionEnd, const TextAttribute attr);
+ const til::point GetSelectionAnchor() const noexcept override;
+ const til::point GetSelectionEnd() const noexcept override;
+ void ColorSelection(const til::point coordSelectionStart, const til::point coordSelectionEnd, const TextAttribute attr) override;
const bool IsUiaDataInitialized() const noexcept override { return true; }
};
diff --git a/src/host/screenInfo.cpp b/src/host/screenInfo.cpp
index 789020ebfc9..048e819a34e 100644
--- a/src/host/screenInfo.cpp
+++ b/src/host/screenInfo.cpp
@@ -1847,21 +1847,17 @@ const SCREEN_INFORMATION& SCREEN_INFORMATION::GetMainBuffer() const
// machine with the main buffer it belongs to.
// TODO: MSFT:19817348 Don't create alt screenbuffer's via an out SCREEN_INFORMATION**
// Parameters:
+// - initAttributes - the attributes the buffer is initialized with.
// - ppsiNewScreenBuffer - a pointer to receive the newly created buffer.
// Return value:
// - STATUS_SUCCESS if handled successfully. Otherwise, an appropriate status code indicating the error.
-[[nodiscard]] NTSTATUS SCREEN_INFORMATION::_CreateAltBuffer(_Out_ SCREEN_INFORMATION** const ppsiNewScreenBuffer)
+[[nodiscard]] NTSTATUS SCREEN_INFORMATION::_CreateAltBuffer(const TextAttribute& initAttributes, _Out_ SCREEN_INFORMATION** const ppsiNewScreenBuffer)
{
// Create new screen buffer.
auto WindowSize = _viewport.Dimensions();
const auto& existingFont = GetCurrentFont();
- // The buffer needs to be initialized with the standard erase attributes,
- // i.e. the current background color, but with no meta attributes set.
- auto initAttributes = GetAttributes();
- initAttributes.SetStandardErase();
-
auto Status = SCREEN_INFORMATION::CreateInstance(WindowSize,
existingFont,
WindowSize,
@@ -1961,10 +1957,10 @@ void SCREEN_INFORMATION::_handleDeferredResize(SCREEN_INFORMATION& siMain)
// screen buffer and an alternate. ASBSET creates a new alternate, and switches to it. If there is an already
// existing alternate, it is discarded. This allows applications to retain one HANDLE, and switch which buffer it points to seamlessly.
// Parameters:
-// - None
+// - initAttributes - the attributes the buffer is initialized with.
// Return value:
// - STATUS_SUCCESS if handled successfully. Otherwise, an appropriate status code indicating the error.
-[[nodiscard]] NTSTATUS SCREEN_INFORMATION::UseAlternateScreenBuffer()
+[[nodiscard]] NTSTATUS SCREEN_INFORMATION::UseAlternateScreenBuffer(const TextAttribute& initAttributes)
{
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& siMain = GetMainBuffer();
@@ -1972,7 +1968,7 @@ void SCREEN_INFORMATION::_handleDeferredResize(SCREEN_INFORMATION& siMain)
_handleDeferredResize(siMain);
SCREEN_INFORMATION* psiNewAltBuffer;
- auto Status = _CreateAltBuffer(&psiNewAltBuffer);
+ auto Status = _CreateAltBuffer(initAttributes, &psiNewAltBuffer);
if (SUCCEEDED_NTSTATUS(Status))
{
// if this is already an alternate buffer, we want to make the new
@@ -2269,43 +2265,23 @@ void SCREEN_INFORMATION::SetViewport(const Viewport& newViewport,
// - S_OK
[[nodiscard]] HRESULT SCREEN_INFORMATION::ClearBuffer()
{
- const auto oldCursorPos = _textBuffer->GetCursor().GetPosition();
- auto sNewTop = oldCursorPos.y;
-
- auto delta = (sNewTop + _viewport.Height()) - (GetBufferSize().Height());
- for (auto i = 0; i < delta; i++)
+ // Rotate the buffer to bring the cursor row to the top of the viewport.
+ const auto cursorPos = _textBuffer->GetCursor().GetPosition();
+ for (auto i = 0; i < cursorPos.y; i++)
{
_textBuffer->IncrementCircularBuffer();
- sNewTop--;
}
- const til::point coordNewOrigin{ 0, sNewTop };
- RETURN_IF_FAILED(SetViewportOrigin(true, coordNewOrigin, true));
-
- // SetViewportOrigin will only move the virtual bottom down, but in this
- // case we need to reset it to the top, so we have to update it explicitly.
- UpdateBottom();
-
- // Place the cursor at the same x coord, on the row that's now the top
- RETURN_IF_FAILED(SetCursorPosition({ oldCursorPos.x, sNewTop }, false));
-
- // Update all the rows in the current viewport with the standard erase attributes,
- // i.e. the current background color, but with no meta attributes set.
- auto fillAttributes = GetAttributes();
- fillAttributes.SetStandardErase();
+ // Erase everything below that point.
+ RETURN_IF_FAILED(SetCursorPosition({ 0, 1 }, false));
+ auto& engine = reinterpret_cast(_stateMachine->Engine());
+ engine.Dispatch().EraseInDisplay(DispatchTypes::EraseType::ToEnd);
- // +1 on the y coord because we don't want to clear the attributes of the
- // cursor row, the one we saved.
- til::point fillPosition{ 0, _viewport.Top() + 1 };
- auto fillLength = gsl::narrow(_viewport.Height() * GetBufferSize().Width());
- auto fillData = OutputCellIterator{ fillAttributes, fillLength };
- Write(fillData, fillPosition, false);
+ // Restore the original cursor x offset, but now on the first row.
+ RETURN_IF_FAILED(SetCursorPosition({ cursorPos.x, 0 }, false));
_textBuffer->TriggerRedrawAll();
- // Also reset the line rendition for the erased rows.
- _textBuffer->ResetLineRenditionRange(_viewport.Top(), _viewport.BottomExclusive());
-
return S_OK;
}
@@ -2633,7 +2609,7 @@ Viewport SCREEN_INFORMATION::GetVirtualViewport() const noexcept
// -
// Return Value:
// - true if the character at the cursor's current position is wide
-bool SCREEN_INFORMATION::CursorIsDoubleWidth() const noexcept
+bool SCREEN_INFORMATION::CursorIsDoubleWidth() const
{
const auto& buffer = GetTextBuffer();
const auto position = buffer.GetCursor().GetPosition();
diff --git a/src/host/screenInfo.hpp b/src/host/screenInfo.hpp
index d67e3f5684c..513c73fac5e 100644
--- a/src/host/screenInfo.hpp
+++ b/src/host/screenInfo.hpp
@@ -157,7 +157,7 @@ class SCREEN_INFORMATION : public ConsoleObjectHeader, public Microsoft::Console
InputBuffer* const GetActiveInputBuffer() const override;
#pragma endregion
- bool CursorIsDoubleWidth() const noexcept;
+ bool CursorIsDoubleWidth() const;
DWORD OutputMode;
WORD ResizingWindow; // > 0 if we should ignore WM_SIZE messages
@@ -193,7 +193,7 @@ class SCREEN_INFORMATION : public ConsoleObjectHeader, public Microsoft::Console
void MakeCursorVisible(const til::point CursorPosition);
- [[nodiscard]] NTSTATUS UseAlternateScreenBuffer();
+ [[nodiscard]] NTSTATUS UseAlternateScreenBuffer(const TextAttribute& initAttributes);
void UseMainScreenBuffer();
SCREEN_INFORMATION& GetMainBuffer();
@@ -254,7 +254,8 @@ class SCREEN_INFORMATION : public ConsoleObjectHeader, public Microsoft::Console
[[nodiscard]] NTSTATUS _InitializeOutputStateMachine();
void _FreeOutputStateMachine();
- [[nodiscard]] NTSTATUS _CreateAltBuffer(_Out_ SCREEN_INFORMATION** const ppsiNewScreenBuffer);
+ [[nodiscard]] NTSTATUS _CreateAltBuffer(const TextAttribute& initAttributes,
+ _Out_ SCREEN_INFORMATION** const ppsiNewScreenBuffer);
bool _IsAltBuffer() const;
bool _IsInPtyMode() const;
diff --git a/src/host/server.h b/src/host/server.h
index 254104e71a7..ebc2ac10091 100644
--- a/src/host/server.h
+++ b/src/host/server.h
@@ -117,7 +117,7 @@ class CONSOLE_INFORMATION :
void SetActiveOutputBuffer(SCREEN_INFORMATION& screenBuffer);
bool HasActiveOutputBuffer() const;
- InputBuffer* const GetActiveInputBuffer() const;
+ InputBuffer* const GetActiveInputBuffer() const override;
bool IsInVtIoMode() const;
bool HasPendingCookedRead() const noexcept;
diff --git a/src/host/srvinit.cpp b/src/host/srvinit.cpp
index e1df6552c17..284f4483820 100644
--- a/src/host/srvinit.cpp
+++ b/src/host/srvinit.cpp
@@ -603,11 +603,9 @@ try
outPipeTheirSide.reset();
signalPipeTheirSide.reset();
- // GH#13211 - Make sure we request win32input mode and that the terminal
- // obeys the resizing quirk. Otherwise, defterm connections to the Terminal
- // are going to have weird resizing, and aren't going to send full fidelity
- // input messages.
- const auto commandLine = fmt::format(FMT_COMPILE(L" --headless --resizeQuirk --win32input --signal {:#x}"),
+ // GH#13211 - Make sure the terminal obeys the resizing quirk. Otherwise,
+ // defterm connections to the Terminal are going to have weird resizing.
+ const auto commandLine = fmt::format(FMT_COMPILE(L" --headless --resizeQuirk --signal {:#x}"),
(int64_t)signalPipeOurSide.release());
ConsoleArguments consoleArgs(commandLine, inPipeOurSide.release(), outPipeOurSide.release());
diff --git a/src/host/telemetry.cpp b/src/host/telemetry.cpp
index ac37440653b..c8318f118a0 100644
--- a/src/host/telemetry.cpp
+++ b/src/host/telemetry.cpp
@@ -34,9 +34,6 @@ Telemetry::Telemetry() :
_rgiProcessFileNameIndex(),
_rguiProcessFileNamesCount(),
_rgiAlphabeticalIndex(),
- _rguiProcessFileNamesCodesCount(),
- _rguiProcessFileNamesFailedCodesCount(),
- _rguiProcessFileNamesFailedOutsideCodesCount(),
_rguiTimesApiUsed(),
_rguiTimesApiUsedAnsi(),
_uiNumberProcessFileNames(0),
@@ -214,28 +211,6 @@ void Telemetry::FindDialogClosed()
_uiFindNextClickedTotal = 0;
}
-// Total up all the used VT100 codes and assign them to the last process that was attached.
-// We originally did this when each process disconnected, but some processes don't
-// disconnect when the conhost process exits. So we have to remember the last process that connected.
-void Telemetry::TotalCodesForPreviousProcess()
-{
- using namespace Microsoft::Console::VirtualTerminal;
- // Get the values even if we aren't recording the previously connected process, since we want to reset them to 0.
- auto _uiTimesUsedCurrent = TermTelemetry::Instance().GetAndResetTimesUsedCurrent();
- auto _uiTimesFailedCurrent = TermTelemetry::Instance().GetAndResetTimesFailedCurrent();
- auto _uiTimesFailedOutsideRangeCurrent = TermTelemetry::Instance().GetAndResetTimesFailedOutsideRangeCurrent();
-
- if (_iProcessConnectedCurrently < c_iMaxProcessesConnected)
- {
- _rguiProcessFileNamesCodesCount[_iProcessConnectedCurrently] += _uiTimesUsedCurrent;
- _rguiProcessFileNamesFailedCodesCount[_iProcessConnectedCurrently] += _uiTimesFailedCurrent;
- _rguiProcessFileNamesFailedOutsideCodesCount[_iProcessConnectedCurrently] += _uiTimesFailedOutsideRangeCurrent;
-
- // Don't total any more process connected telemetry, unless a new processes attaches that we want to gather.
- _iProcessConnectedCurrently = SIZE_MAX;
- }
-}
-
// Tries to find the process name amongst our previous process names by doing a binary search.
// The main difference between this and the standard bsearch library call, is that if this
// can't find the string, it returns the position the new string should be inserted at. This saves
@@ -284,8 +259,6 @@ void Telemetry::LogProcessConnected(const HANDLE hProcess)
// This is a bit of processing, so don't do it for the 95% of machines that aren't being sampled.
if (TraceLoggingProviderEnabled(g_hConhostV2EventTraceProvider, 0, MICROSOFT_KEYWORD_MEASURES))
{
- TotalCodesForPreviousProcess();
-
// Don't initialize wszFilePathAndName, QueryFullProcessImageName does that for us. Use QueryFullProcessImageName instead of
// GetProcessImageFileName because we need the path to begin with a drive letter and not a device name.
WCHAR wszFilePathAndName[MAX_PATH];
@@ -360,15 +333,8 @@ void Telemetry::WriteFinalTraceLog()
// This is a bit of processing, so don't do it for the 95% of machines that aren't being sampled.
if (TraceLoggingProviderEnabled(g_hConhostV2EventTraceProvider, 0, MICROSOFT_KEYWORD_MEASURES))
{
- // Normally we would set the activity Id earlier, but since we know the parser only sends
- // one final log at the end, setting the activity this late should be fine.
- Microsoft::Console::VirtualTerminal::TermTelemetry::Instance().SetActivityId(_activity.Id());
- Microsoft::Console::VirtualTerminal::TermTelemetry::Instance().SetShouldWriteFinalLog(_fUserInteractiveForTelemetry);
-
if (_fUserInteractiveForTelemetry)
{
- TotalCodesForPreviousProcess();
-
// Send this back using "measures" since we want a good sampling of our entire userbase.
time_t tEndedAt;
time(&tEndedAt);
@@ -395,9 +361,6 @@ void Telemetry::WriteFinalTraceLog()
// Casting to UINT should be fine, since our array size is only 2K.
TraceLoggingPackedField(_wchProcessFileNames, static_cast(sizeof(WCHAR) * _iProcessFileNamesNext), TlgInUNICODESTRING | TlgInVcount, "ProcessesConnected"),
TraceLoggingUInt32Array(_rguiProcessFileNamesCount, _uiNumberProcessFileNames, "ProcessesConnectedCount"),
- TraceLoggingUInt32Array(_rguiProcessFileNamesCodesCount, _uiNumberProcessFileNames, "ProcessesConnectedCodesCount"),
- TraceLoggingUInt32Array(_rguiProcessFileNamesFailedCodesCount, _uiNumberProcessFileNames, "ProcessesConnectedFailedCodesCount"),
- TraceLoggingUInt32Array(_rguiProcessFileNamesFailedOutsideCodesCount, _uiNumberProcessFileNames, "ProcessesConnectedFailedOutsideCount"),
// Send back both starting and ending times separately instead just usage time (ending - starting).
// This can help us determine if they were using multiple consoles at the same time.
TraceLoggingInt32(static_cast(_tStartedAt), "StartedUsingAtSeconds"),
diff --git a/src/host/telemetry.hpp b/src/host/telemetry.hpp
index e35659cb3f1..45cbea796a4 100644
--- a/src/host/telemetry.hpp
+++ b/src/host/telemetry.hpp
@@ -133,7 +133,6 @@ class Telemetry
void operator=(const Telemetry&);
bool FindProcessName(const WCHAR* pszProcessName, _Out_ size_t* iPosition) const;
- void TotalCodesForPreviousProcess();
static const int c_iMaxProcessesConnected = 100;
@@ -159,12 +158,6 @@ class Telemetry
unsigned int _rguiProcessFileNamesCount[c_iMaxProcessesConnected];
// To speed up searching the Process Names, create an alphabetically sorted index.
size_t _rgiAlphabeticalIndex[c_iMaxProcessesConnected];
- // Total of how many codes each process used
- unsigned int _rguiProcessFileNamesCodesCount[c_iMaxProcessesConnected];
- // Total of how many failed codes each process used
- unsigned int _rguiProcessFileNamesFailedCodesCount[c_iMaxProcessesConnected];
- // Total of how many failed codes each process used outside the valid range.
- unsigned int _rguiProcessFileNamesFailedOutsideCodesCount[c_iMaxProcessesConnected];
unsigned int _rguiTimesApiUsed[NUMBER_OF_APIS];
// Most of this array will be empty, and is only used if an API has an ansi specific variant.
unsigned int _rguiTimesApiUsedAnsi[NUMBER_OF_APIS];
diff --git a/src/host/ut_host/ScreenBufferTests.cpp b/src/host/ut_host/ScreenBufferTests.cpp
index 1209780e2be..b26801be0a1 100644
--- a/src/host/ut_host/ScreenBufferTests.cpp
+++ b/src/host/ut_host/ScreenBufferTests.cpp
@@ -262,6 +262,8 @@ class ScreenBufferTests
TEST_METHOD(CopyDoubleWidthRectangularArea);
TEST_METHOD(DelayedWrapReset);
+
+ TEST_METHOD(EraseColorMode);
};
void ScreenBufferTests::SingleAlternateBufferCreationTest()
@@ -275,7 +277,7 @@ void ScreenBufferTests::SingleAlternateBufferCreationTest()
VERIFY_IS_NULL(psiOriginal->_psiAlternateBuffer);
VERIFY_IS_NULL(psiOriginal->_psiMainBuffer);
- auto Status = psiOriginal->UseAlternateScreenBuffer();
+ auto Status = psiOriginal->UseAlternateScreenBuffer({});
if (VERIFY_NT_SUCCESS(Status))
{
Log::Comment(L"First alternate buffer successfully created");
@@ -308,7 +310,7 @@ void ScreenBufferTests::MultipleAlternateBufferCreationTest()
L"main buffer.");
const auto psiOriginal = &gci.GetActiveOutputBuffer();
- auto Status = psiOriginal->UseAlternateScreenBuffer();
+ auto Status = psiOriginal->UseAlternateScreenBuffer({});
if (VERIFY_NT_SUCCESS(Status))
{
Log::Comment(L"First alternate buffer successfully created");
@@ -319,7 +321,7 @@ void ScreenBufferTests::MultipleAlternateBufferCreationTest()
VERIFY_IS_NULL(psiOriginal->_psiMainBuffer);
VERIFY_IS_NULL(psiFirstAlternate->_psiAlternateBuffer);
- Status = psiFirstAlternate->UseAlternateScreenBuffer();
+ Status = psiFirstAlternate->UseAlternateScreenBuffer({});
if (VERIFY_NT_SUCCESS(Status))
{
Log::Comment(L"Second alternate buffer successfully created");
@@ -353,7 +355,7 @@ void ScreenBufferTests::MultipleAlternateBuffersFromMainCreationTest()
L"Testing creating one alternate buffer, then creating another"
L" alternate from the main, before returning to the main buffer.");
const auto psiOriginal = &gci.GetActiveOutputBuffer();
- auto Status = psiOriginal->UseAlternateScreenBuffer();
+ auto Status = psiOriginal->UseAlternateScreenBuffer({});
if (VERIFY_NT_SUCCESS(Status))
{
Log::Comment(L"First alternate buffer successfully created");
@@ -364,7 +366,7 @@ void ScreenBufferTests::MultipleAlternateBuffersFromMainCreationTest()
VERIFY_IS_NULL(psiOriginal->_psiMainBuffer);
VERIFY_IS_NULL(psiFirstAlternate->_psiAlternateBuffer);
- Status = psiOriginal->UseAlternateScreenBuffer();
+ Status = psiOriginal->UseAlternateScreenBuffer({});
if (VERIFY_NT_SUCCESS(Status))
{
Log::Comment(L"Second alternate buffer successfully created");
@@ -409,7 +411,7 @@ void ScreenBufferTests::AlternateBufferCursorInheritanceTest()
mainCursor.SetBlinkingAllowed(mainCursorBlinking);
Log::Comment(L"Switch to the alternate buffer.");
- VERIFY_SUCCEEDED(mainBuffer.UseAlternateScreenBuffer());
+ VERIFY_SUCCEEDED(mainBuffer.UseAlternateScreenBuffer({}));
auto& altBuffer = gci.GetActiveOutputBuffer();
auto& altCursor = altBuffer.GetTextBuffer().GetCursor();
auto useMain = wil::scope_exit([&] { altBuffer.UseMainScreenBuffer(); });
@@ -901,7 +903,7 @@ void ScreenBufferTests::TestAltBufferTabStops()
_SetTabStops(mainBuffer, expectedStops, true);
VERIFY_ARE_EQUAL(expectedStops, _GetTabStops(mainBuffer));
- VERIFY_SUCCEEDED(mainBuffer.UseAlternateScreenBuffer());
+ VERIFY_SUCCEEDED(mainBuffer.UseAlternateScreenBuffer({}));
auto& altBuffer = gci.GetActiveOutputBuffer();
auto useMain = wil::scope_exit([&] { altBuffer.UseMainScreenBuffer(); });
@@ -2449,7 +2451,7 @@ void ScreenBufferTests::TestAltBufferCursorState()
VERIFY_IS_NULL(original._psiAlternateBuffer);
VERIFY_IS_NULL(original._psiMainBuffer);
- auto Status = original.UseAlternateScreenBuffer();
+ auto Status = original.UseAlternateScreenBuffer({});
if (VERIFY_NT_SUCCESS(Status))
{
Log::Comment(L"Alternate buffer successfully created");
@@ -2494,7 +2496,7 @@ void ScreenBufferTests::TestAltBufferVtDispatching()
VERIFY_IS_NULL(mainBuffer._psiAlternateBuffer);
VERIFY_IS_NULL(mainBuffer._psiMainBuffer);
- auto Status = mainBuffer.UseAlternateScreenBuffer();
+ auto Status = mainBuffer.UseAlternateScreenBuffer({});
if (VERIFY_NT_SUCCESS(Status))
{
Log::Comment(L"Alternate buffer successfully created");
@@ -2893,7 +2895,7 @@ void ScreenBufferTests::BackspaceDefaultAttrsWriteCharsLegacy()
{
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Data:writeSingly", L"{false, true}")
- TEST_METHOD_PROPERTY(L"Data:writeCharsLegacyMode", L"{0, 1, 2, 3, 4, 5, 6, 7}")
+ TEST_METHOD_PROPERTY(L"Data:writeCharsLegacyMode", L"{0, 1, 2}")
END_TEST_METHOD_PROPERTIES();
bool writeSingly;
@@ -3079,7 +3081,7 @@ void ScreenBufferTests::SetGlobalColorTable()
Log::Comment(NoThrowString().Format(L"Create an alt buffer"));
- VERIFY_SUCCEEDED(mainBuffer.UseAlternateScreenBuffer());
+ VERIFY_SUCCEEDED(mainBuffer.UseAlternateScreenBuffer({}));
auto& altBuffer = gci.GetActiveOutputBuffer();
auto useMain = wil::scope_exit([&] { altBuffer.UseMainScreenBuffer(); });
@@ -3182,7 +3184,7 @@ void ScreenBufferTests::SetColorTableThreeDigits()
Log::Comment(NoThrowString().Format(L"Create an alt buffer"));
- VERIFY_SUCCEEDED(mainBuffer.UseAlternateScreenBuffer());
+ VERIFY_SUCCEEDED(mainBuffer.UseAlternateScreenBuffer({}));
auto& altBuffer = gci.GetActiveOutputBuffer();
auto useMain = wil::scope_exit([&] { altBuffer.UseMainScreenBuffer(); });
@@ -5800,7 +5802,7 @@ void ScreenBufferTests::RestoreDownAltBufferWithTerminalScrolling()
VERIFY_IS_NULL(siMain._psiAlternateBuffer);
Log::Comment(L"Create an alternate buffer");
- if (VERIFY_NT_SUCCESS(siMain.UseAlternateScreenBuffer()))
+ if (VERIFY_NT_SUCCESS(siMain.UseAlternateScreenBuffer({})))
{
VERIFY_IS_NOT_NULL(siMain._psiAlternateBuffer);
auto& altBuffer = *siMain._psiAlternateBuffer;
@@ -5965,7 +5967,7 @@ void ScreenBufferTests::ClearAlternateBuffer()
VerifyText(siMain.GetTextBuffer());
Log::Comment(L"Create an alternate buffer");
- if (VERIFY_NT_SUCCESS(siMain.UseAlternateScreenBuffer()))
+ if (VERIFY_NT_SUCCESS(siMain.UseAlternateScreenBuffer({})))
{
VERIFY_IS_NOT_NULL(siMain._psiAlternateBuffer);
auto& altBuffer = *siMain._psiAlternateBuffer;
@@ -8176,7 +8178,7 @@ void ScreenBufferTests::CopyDoubleWidthRectangularArea()
// Make the second line (offset 1) double width.
textBuffer.GetCursor().SetPosition({ 0, 1 });
- textBuffer.SetCurrentLineRendition(LineRendition::DoubleWidth);
+ textBuffer.SetCurrentLineRendition(LineRendition::DoubleWidth, activeAttr);
// Copy a segment of the top three lines with DECCRA.
stateMachine.ProcessString(L"\033[1;31;3;50;1;4;31;1$v");
@@ -8306,3 +8308,89 @@ void ScreenBufferTests::DelayedWrapReset()
VERIFY_ARE_EQUAL(expectedPos, actualPos);
}
}
+
+void ScreenBufferTests::EraseColorMode()
+{
+ BEGIN_TEST_METHOD_PROPERTIES()
+ TEST_METHOD_PROPERTY(L"Data:op", L"{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19}")
+ TEST_METHOD_PROPERTY(L"Data:eraseMode", L"{false,true}")
+ END_TEST_METHOD_PROPERTIES();
+
+ bool eraseMode;
+ VERIFY_SUCCEEDED(TestData::TryGetValue(L"eraseMode", eraseMode));
+ int opIndex;
+ VERIFY_SUCCEEDED(TestData::TryGetValue(L"op", opIndex));
+
+ auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
+ auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer();
+ auto& stateMachine = si.GetStateMachine();
+ WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING);
+ WI_SetFlag(si.OutputMode, DISABLE_NEWLINE_AUTO_RETURN);
+
+ const auto bufferWidth = si.GetBufferSize().Width();
+ const auto bufferHeight = si.GetBufferSize().Height();
+
+ // Set the viewport to 24 lines.
+ si.SetViewport(Viewport::FromDimensions({ 0, 0 }, { bufferWidth, 24 }), true);
+ const auto viewport = si.GetViewport();
+
+ // Fill the buffer with text. Red on Blue.
+ const auto bufferChars = L"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn";
+ const auto bufferAttr = TextAttribute{ FOREGROUND_RED | BACKGROUND_BLUE };
+ _FillLines(0, bufferHeight, bufferChars, bufferAttr);
+
+ // Set the active attributes with a mix of color and meta attributes.
+ auto activeAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) };
+ activeAttr.SetCrossedOut(true);
+ activeAttr.SetReverseVideo(true);
+ activeAttr.SetUnderlined(true);
+ si.SetAttributes(activeAttr);
+
+ // By default, the meta attributes are expected to be cleared when erasing.
+ auto standardEraseAttr = activeAttr;
+ standardEraseAttr.SetStandardErase();
+
+ const struct
+ {
+ std::wstring_view name;
+ std::wstring_view sequence;
+ til::point startPos = {};
+ til::point erasePos = {};
+ } ops[] = {
+ { L"LF", L"\n", { 0, 23 }, { 0, 24 } }, // pans down on last row
+ { L"VT", L"\v", { 0, 23 }, { 0, 24 } }, // pans down on last row
+ { L"FF", L"\f", { 0, 23 }, { 0, 24 } }, // pans down on last row
+ { L"NEL", L"\033E", { 0, 23 }, { 0, 24 } }, // pans down on last row
+ { L"IND", L"\033D", { 0, 23 }, { 0, 24 } }, // pans down on last row
+ { L"RI", L"\033M" },
+ { L"DECBI", L"\033\066" },
+ { L"DECFI", L"\033\071", { 79, 0 }, { 79, 0 } }, // scrolls in last column
+ { L"DECIC", L"\033['}" },
+ { L"DECDC", L"\033['~", {}, { 79, 0 } }, // last column erased
+ { L"ICH", L"\033[@" },
+ { L"DCH", L"\033[P", {}, { 79, 0 } }, // last column erased
+ { L"IL", L"\033[L" },
+ { L"DL", L"\033[M", {}, { 0, 23 } }, // last row erased
+ { L"ECH", L"\033[X" },
+ { L"EL", L"\033[K" },
+ { L"ED", L"\033[J" },
+ { L"DECERA", L"\033[$z" },
+ { L"SU", L"\033[S", {}, { 0, 23 } }, // last row erased
+ { L"SD", L"\033[T" },
+ };
+ const auto& op = gsl::at(ops, opIndex);
+
+ si.GetTextBuffer().GetCursor().SetPosition(op.startPos);
+
+ Log::Comment(eraseMode ? L"Enable DECECM" : L"Disable DECECM");
+ stateMachine.ProcessString(eraseMode ? L"\033[?117h" : L"\033[?117l");
+
+ Log::Comment(NoThrowString().Format(L"Execute %s", op.name.data()));
+ stateMachine.ProcessString(op.sequence);
+
+ Log::Comment(L"Verify expected erase attributes");
+ const auto expectedEraseAttr = eraseMode ? TextAttribute{} : standardEraseAttr;
+ const auto cellData = si.GetCellDataAt(op.erasePos);
+ VERIFY_ARE_EQUAL(expectedEraseAttr, cellData->TextAttr());
+ VERIFY_ARE_EQUAL(L" ", cellData->Chars());
+}
diff --git a/src/host/ut_host/TextBufferTests.cpp b/src/host/ut_host/TextBufferTests.cpp
index 823736ea30f..bcb4cec4a99 100644
--- a/src/host/ut_host/TextBufferTests.cpp
+++ b/src/host/ut_host/TextBufferTests.cpp
@@ -188,7 +188,7 @@ void TextBufferTests::TestWrapFlag()
{
auto& textBuffer = GetTbi();
- auto& Row = textBuffer._GetFirstRow();
+ auto& Row = textBuffer.GetRowByOffset(0);
// no wrap by default
VERIFY_IS_FALSE(Row.WasWrapForced());
@@ -207,7 +207,7 @@ void TextBufferTests::TestWrapThroughWriteLine()
auto& textBuffer = GetTbi();
auto VerifyWrap = [&](bool expected) {
- auto& Row = textBuffer._GetFirstRow();
+ auto& Row = textBuffer.GetRowByOffset(0);
if (expected)
{
@@ -278,7 +278,7 @@ void TextBufferTests::TestDoubleBytePadFlag()
{
auto& textBuffer = GetTbi();
- auto& Row = textBuffer._GetFirstRow();
+ auto& Row = textBuffer.GetRowByOffset(0);
// no padding by default
VERIFY_IS_FALSE(Row.WasDoubleBytePadded());
@@ -300,7 +300,7 @@ void TextBufferTests::DoBoundaryTest(PCWCHAR const pwszInputString,
{
auto& textBuffer = GetTbi();
- auto& row = textBuffer._GetFirstRow();
+ auto& row = textBuffer.GetRowByOffset(0);
// copy string into buffer
for (til::CoordType i = 0; i < cLength; ++i)
@@ -622,7 +622,7 @@ void TextBufferTests::TestIncrementCircularBuffer()
textBuffer._firstRow = iRowToTestIndex;
// fill first row with some stuff
- auto& FirstRow = textBuffer._GetFirstRow();
+ auto& FirstRow = textBuffer.GetRowByOffset(0);
FirstRow.ReplaceCharacters(0, 1, { L"A" });
// ensure it does say that it contains text
@@ -633,7 +633,7 @@ void TextBufferTests::TestIncrementCircularBuffer()
// validate that first row has moved
VERIFY_ARE_EQUAL(textBuffer._firstRow, iNextRowIndex); // first row has incremented
- VERIFY_ARE_NOT_EQUAL(textBuffer._GetFirstRow(), FirstRow); // the old first row is no longer the first
+ VERIFY_ARE_NOT_EQUAL(textBuffer.GetRowByOffset(0), FirstRow); // the old first row is no longer the first
// ensure old first row has been emptied
VERIFY_IS_FALSE(FirstRow.ContainsText());
@@ -1847,7 +1847,7 @@ void TextBufferTests::ResizeTraditionalRotationPreservesHighUnicode()
// This is the negative squared latin capital letter B emoji: 🅱
// It's encoded in UTF-16, as needed by the buffer.
const auto bButton = L"\xD83C\xDD71";
- _buffer->_storage[pos.y].ReplaceCharacters(pos.x, 2, bButton);
+ _buffer->GetRowByOffset(pos.y).ReplaceCharacters(pos.x, 2, bButton);
// Read back the text at that position and ensure that it matches what we wrote.
const auto readBack = _buffer->GetTextDataAt(pos);
@@ -1888,7 +1888,7 @@ void TextBufferTests::ScrollBufferRotationPreservesHighUnicode()
// This is the fire emoji: 🔥
// It's encoded in UTF-16, as needed by the buffer.
const auto fire = L"\xD83D\xDD25";
- _buffer->_storage[pos.y].ReplaceCharacters(pos.x, 2, fire);
+ _buffer->GetRowByOffset(pos.y).ReplaceCharacters(pos.x, 2, fire);
// Read back the text at that position and ensure that it matches what we wrote.
const auto readBack = _buffer->GetTextDataAt(pos);
@@ -1902,11 +1902,7 @@ void TextBufferTests::ScrollBufferRotationPreservesHighUnicode()
// Scroll the row with our data by delta.
_buffer->ScrollRows(pos.y, 1, delta);
- // Retrieve the text at the old and new positions.
- const auto shouldBeEmptyText = *_buffer->GetTextDataAt(pos);
const auto shouldBeFireText = *_buffer->GetTextDataAt(newPos);
-
- VERIFY_ARE_EQUAL(String(L" "), String(shouldBeEmptyText.data(), gsl::narrow(shouldBeEmptyText.size())));
VERIFY_ARE_EQUAL(String(fire), String(shouldBeFireText.data(), gsl::narrow