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 @@ --> false nested + + DoNotGenerateOtherProviders true 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) true precomp.h @@ -135,10 +136,10 @@ false true true + true stdcpp20 stdc17 - - %(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:throwingNew Guard 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(shouldBeFireText.size()))); } @@ -1927,7 +1923,7 @@ void TextBufferTests::ResizeTraditionalHighUnicodeRowRemoval() // This is the eggplant emoji: 🍆 // It's encoded in UTF-16, as needed by the buffer. const auto emoji = L"\xD83C\xDF46"; - _buffer->_storage[pos.y].ReplaceCharacters(pos.x, 2, emoji); + _buffer->GetRowByOffset(pos.y).ReplaceCharacters(pos.x, 2, emoji); // Read back the text at that position and ensure that it matches what we wrote. const auto readBack = _buffer->GetTextDataAt(pos); @@ -1957,7 +1953,7 @@ void TextBufferTests::ResizeTraditionalHighUnicodeColumnRemoval() // This is the peach emoji: 🍑 // It's encoded in UTF-16, as needed by the buffer. const auto emoji = L"\xD83C\xDF51"; - _buffer->_storage[pos.y].ReplaceCharacters(pos.x, 2, emoji); + _buffer->GetRowByOffset(pos.y).ReplaceCharacters(pos.x, 2, emoji); // Read back the text at that position and ensure that it matches what we wrote. const auto readBack = _buffer->GetTextDataAt(pos); diff --git a/src/host/ut_host/VtIoTests.cpp b/src/host/ut_host/VtIoTests.cpp index 2b4d2fe5b39..329dd3f46ec 100644 --- a/src/host/ut_host/VtIoTests.cpp +++ b/src/host/ut_host/VtIoTests.cpp @@ -325,7 +325,7 @@ class MockRenderData : public IRenderData return 12ul; } - bool IsCursorDoubleWidth() const noexcept override + bool IsCursorDoubleWidth() const override { return false; } diff --git a/src/inc/conpty-static.h b/src/inc/conpty-static.h index 89b20468408..4d22048868c 100644 --- a/src/inc/conpty-static.h +++ b/src/inc/conpty-static.h @@ -24,7 +24,6 @@ #endif #define PSEUDOCONSOLE_RESIZE_QUIRK (2u) -#define PSEUDOCONSOLE_WIN32_INPUT_MODE (4u) #define PSEUDOCONSOLE_PASSTHROUGH_MODE (8u) CONPTY_EXPORT HRESULT WINAPI ConptyCreatePseudoConsole(COORD size, HANDLE hInput, HANDLE hOutput, DWORD dwFlags, HPCON* phPC); diff --git a/src/inc/til.h b/src/inc/til.h index f7bcc5ac119..ee4f29df34c 100644 --- a/src/inc/til.h +++ b/src/inc/til.h @@ -3,6 +3,15 @@ #pragma once +// This is a copy of how DirectXMath.h determines _XM_SSE_INTRINSICS_ and _XM_ARM_NEON_INTRINSICS_. +#if (defined(_M_IX86) || defined(_M_X64) || __i386__ || __x86_64__) && !defined(_M_HYBRID_X86_ARM64) && !defined(_M_ARM64EC) +#define TIL_SSE_INTRINSICS +#elif defined(_M_ARM) || defined(_M_ARM64) || defined(_M_HYBRID_X86_ARM64) || defined(_M_ARM64EC) || __arm__ || __aarch64__ +#define TIL_ARM_NEON_INTRINSICS +#else +#define TIL_NO_INTRINSICS +#endif + #define _TIL_INLINEPREFIX __declspec(noinline) inline #include "til/at.h" diff --git a/src/inc/til/bitmap.h b/src/inc/til/bitmap.h index 438e1b946fb..38213500477 100644 --- a/src/inc/til/bitmap.h +++ b/src/inc/til/bitmap.h @@ -351,20 +351,26 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" void set(const til::point pt) { - THROW_HR_IF(E_INVALIDARG, !_rc.contains(pt)); - _runs.reset(); // reset cached runs on any non-const method - - _bits.set(_rc.index_of(pt)); + if (_rc.contains(pt)) + { + _runs.reset(); // reset cached runs on any non-const method + _bits.set(_rc.index_of(pt)); + } } - void set(const til::rect& rc) + void set(til::rect rc) { - THROW_HR_IF(E_INVALIDARG, !_rc.contains(rc)); _runs.reset(); // reset cached runs on any non-const method - for (auto row = rc.top; row < rc.bottom; ++row) + rc &= _rc; + + const auto width = rc.width(); + const auto stride = _rc.width(); + auto idx = _rc.index_of({ rc.left, rc.top }); + + for (auto row = rc.top; row < rc.bottom; ++row, idx += stride) { - _bits.set(_rc.index_of(til::point{ rc.left, row }), rc.width(), true); + _bits.set(idx, width, true); } } diff --git a/src/inc/til/rle.h b/src/inc/til/rle.h index fa9e3793678..d94c3a744e6 100644 --- a/src/inc/til/rle.h +++ b/src/inc/til/rle.h @@ -402,6 +402,11 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" return _runs; } + container& runs() noexcept + { + return _runs; + } + // Get the value at the position const_reference at(size_type position) const { diff --git a/src/inc/til/small_vector.h b/src/inc/til/small_vector.h index 4a1fe4eab90..2a10dcb6b36 100644 --- a/src/inc/til/small_vector.h +++ b/src/inc/til/small_vector.h @@ -330,7 +330,9 @@ namespace til using const_reverse_iterator = std::reverse_iterator; small_vector() noexcept : - _data{ &_buffer[0] } + _data{ &_buffer[0] }, + _capacity{ N }, + _size{ 0 } { } @@ -357,57 +359,39 @@ namespace til small_vector(const small_vector& other) : small_vector{} { - operator=(other); + _copy_assign(other); } // NOTE: If an exception is thrown while copying, the vector is left empty. small_vector& operator=(const small_vector& other) { - clear(); - reserve(other._size); - - std::uninitialized_copy(other.begin(), other.end(), _uninitialized_begin()); - _size = other._size; + if (this != &other) + { + clear(); + _copy_assign(other); + } return *this; } - small_vector(small_vector&& other) noexcept : - small_vector{} + small_vector(small_vector&& other) noexcept { - operator=(std::move(other)); + _move_assign(other); } small_vector& operator=(small_vector&& other) noexcept { - std::destroy(begin(), end()); - if (_capacity != N) + if (this != &other) { - _deallocate(_data); - } + std::destroy(begin(), end()); + if (_capacity != N) + { + _deallocate(_data); + } - if (other._capacity == N) - { - _data = &_buffer[0]; - _capacity = N; - _size = other._size; - // The earlier static_assert(std::is_nothrow_move_constructible_v) - // ensures that we don't exit in a weird state with invalid `_size`. -#pragma warning(suppress : 26447) // The function is declared 'noexcept' but calls function '...' which may throw exceptions (f.6). - std::uninitialized_move(other.begin(), other.end(), _uninitialized_begin()); - std::destroy(other.begin(), other.end()); - } - else - { - _data = other._data; - _capacity = other._capacity; - _size = other._size; + _move_assign(other); } - other._data = &other._buffer[0]; - other._capacity = N; - other._size = 0; - return *this; } @@ -622,6 +606,24 @@ namespace til _capacity = capacity; } + // This is a very unsafe shortcut to free the buffer and get a direct + // hold to the _buffer. The caller can then fill it with `size` items. + [[nodiscard]] T* unsafe_shrink_to_size(size_t size) noexcept + { + assert(size <= N); + + if (_capacity != N) + { + _deallocate(_data); + } + + _data = &_buffer[0]; + _capacity = N; + _size = size; + + return &_buffer[0]; + } + void push_back(const T& value) { emplace_back(value); @@ -758,6 +760,38 @@ namespace til #endif } + void _copy_assign(const small_vector& other) + { + reserve(other._size); + std::uninitialized_copy(other.begin(), other.end(), _uninitialized_begin()); + _size = other._size; + } + + void _move_assign(small_vector& other) noexcept + { + if (other._capacity == N) + { + _data = &_buffer[0]; + _capacity = N; + _size = other._size; + // The earlier static_assert(std::is_nothrow_move_constructible_v) + // ensures that we don't exit in a weird state with invalid `_size`. +#pragma warning(suppress : 26447) // The function is declared 'noexcept' but calls function '...' which may throw exceptions (f.6). + std::uninitialized_move(other.begin(), other.end(), _uninitialized_begin()); + std::destroy(other.begin(), other.end()); + } + else + { + _data = other._data; + _capacity = other._capacity; + _size = other._size; + } + + other._data = &other._buffer[0]; + other._capacity = N; + other._size = 0; + } + size_type _ensure_fits(size_type add) { const auto new_size = _size + add; @@ -880,8 +914,8 @@ namespace til } T* _data; - size_t _capacity = N; - size_t _size = 0; + size_t _capacity; + size_t _size; T _buffer[N]; }; } diff --git a/src/inc/til/static_map.h b/src/inc/til/static_map.h index f5d82f113a8..765e53ba5f2 100644 --- a/src/inc/til/static_map.h +++ b/src/inc/til/static_map.h @@ -8,13 +8,8 @@ // There is no requirement that keys be sorted, as it will // use constexpr std::sort during construction. // -// Until we can use C++20, this is no cheaper than using -// a static std::unordered_map that is initialized at -// startup or on first use. -// To build something that can be constexpr as of C++17, -// use til::presorted_static_map and make certain that -// your pairs are sorted as they would have been sorted -// by your comparator. +// Use til::presorted_static_map and make certain that +// your pairs are sorted if you want to skip the std::sort. // A failure to sort your keys will result in unusual // runtime behavior, but no error messages will be // generated. @@ -32,31 +27,30 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" }; } - template, size_t N = 0, typename SortedInput = details::unsorted_input_t> + template class static_map { public: using const_iterator = typename std::array, N>::const_iterator; template - constexpr explicit static_map(const Args&... args) noexcept : - _predicate{}, - _array{ { args... } } + constexpr explicit static_map(Args&&... args) noexcept : + _array{ { std::forward(args)... } } { static_assert(sizeof...(Args) == N); if constexpr (!SortedInput::value) { - const auto compareKeys = [&](const auto& p1, const auto& p2) { return _predicate(p1.first, p2.first); }; - std::sort(_array.begin(), _array.end(), compareKeys); // compile-time sorting starting C++20 + std::sort(_array.begin(), _array.end()); } } - [[nodiscard]] constexpr const_iterator find(const K& key) const noexcept + [[nodiscard]] constexpr const_iterator find(const auto& key) const noexcept { - const auto compareKey = [&](const auto& p) { return _predicate(p.first, key); }; - const auto iter{ std::partition_point(_array.begin(), _array.end(), compareKey) }; + const auto iter = std::partition_point(_array.begin(), _array.end(), [&](const auto& p) { + return p.first < key; + }); - if (iter == _array.end() || _predicate(key, iter->first)) + if (iter == _array.end() || key != iter->first) { return _array.end(); } @@ -69,7 +63,7 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" return _array.end(); } - [[nodiscard]] constexpr const V& at(const K& key) const + [[nodiscard]] constexpr const V& at(const auto& key) const { const auto iter{ find(key) }; @@ -87,25 +81,26 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" } private: - Compare _predicate; std::array, N> _array; }; - template, size_t N = 0> - class presorted_static_map : public static_map + template + class presorted_static_map : public static_map { public: template - constexpr explicit presorted_static_map(const Args&... args) noexcept : - static_map{ args... } {}; + constexpr explicit presorted_static_map(Args&&... args) noexcept : + static_map{ std::forward(args)... } + { + } }; // this is a deduction guide that ensures two things: // 1. static_map's member types are all the same // 2. static_map's fourth template argument (otherwise undeduced) is how many pairs it contains template - static_map(First, Rest...) -> static_map...>, typename First::first_type, void>, typename First::second_type, std::less, 1 + sizeof...(Rest), details::unsorted_input_t>; + static_map(First, Rest...) -> static_map...>, typename First::first_type, void>, typename First::second_type, 1 + sizeof...(Rest)>; template - presorted_static_map(First, Rest...) -> presorted_static_map...>, typename First::first_type, void>, typename First::second_type, std::less, 1 + sizeof...(Rest)>; + presorted_static_map(First, Rest...) -> presorted_static_map...>, typename First::first_type, void>, typename First::second_type, 1 + sizeof...(Rest)>; } diff --git a/src/interactivity/win32/windowio.cpp b/src/interactivity/win32/windowio.cpp index 3d5643764ce..5b882feeeef 100644 --- a/src/interactivity/win32/windowio.cpp +++ b/src/interactivity/win32/windowio.cpp @@ -30,10 +30,6 @@ using Microsoft::Console::Interactivity::ServiceLocator; // Bit 29 is whether ALT was held when the message was posted. #define WM_SYSKEYDOWN_ALT_PRESSED (0x20000000) -// 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) }; - // ---------------------------- // Helpers // ---------------------------- @@ -119,30 +115,8 @@ bool HandleTerminalMouseEvent(const til::point cMousePosition, const short sModifierKeystate, const short sWheelDelta) { - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // If the modes don't align, this is unhandled by default. - auto fWasHandled = false; - - // Virtual terminal input mode - if (IsInVirtualTerminalInputMode()) - { - 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)]. - auto clampedPosition{ cMousePosition }; - const auto clampViewport{ gci.GetActiveOutputBuffer().GetViewport().ToOrigin() }; - clampViewport.Clamp(clampedPosition); - fWasHandled = gci.GetActiveInputBuffer()->GetTerminalInput().HandleMouse(clampedPosition, uiButton, sModifierKeystate, sWheelDelta, state); - } - - return fWasHandled; + const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + return gci.pInputBuffer->WriteMouseEvent(cMousePosition, uiButton, sModifierKeystate, sWheelDelta); } void HandleKeyEvent(const HWND hWnd, diff --git a/src/renderer/atlas/AtlasEngine.api.cpp b/src/renderer/atlas/AtlasEngine.api.cpp index 817d94e5929..098a389764c 100644 --- a/src/renderer/atlas/AtlasEngine.api.cpp +++ b/src/renderer/atlas/AtlasEngine.api.cpp @@ -420,14 +420,19 @@ void AtlasEngine::SetWarningCallback(std::function pfn) noexcept [[nodiscard]] HRESULT AtlasEngine::SetWindowSize(const til::size pixels) noexcept { - u16x2 newSize; - RETURN_IF_FAILED(vec2_narrow(pixels.width, pixels.height, newSize)); - - // At the time of writing: - // When Win+D is pressed, `TriggerRedrawCursor` is called and a render pass is initiated. - // As conhost is in the background, GetClientRect will return {0,0} and we'll get called with {0,0}. - // This isn't a valid value for _api.sizeInPixel and would crash _recreateSizeDependentResources(). - if (_api.s->targetSize != newSize && newSize != u16x2{}) + // When Win+D is pressed, `GetClientRect` returns {0,0}. + // There's probably more situations in which our callers may pass us invalid data. + if (!pixels) + { + return S_OK; + } + + const u16x2 newSize{ + gsl::narrow_cast(clamp(pixels.width, 1, u16max)), + gsl::narrow_cast(clamp(pixels.height, 1, u16max)), + }; + + if (_api.s->targetSize != newSize) { _api.s.write()->targetSize = newSize; } diff --git a/src/renderer/atlas/BackendD2D.cpp b/src/renderer/atlas/BackendD2D.cpp index 1d0db35803b..7155d842ad4 100644 --- a/src/renderer/atlas/BackendD2D.cpp +++ b/src/renderer/atlas/BackendD2D.cpp @@ -75,6 +75,7 @@ void BackendD2D::_handleSettingsUpdate(const RenderingPayload& p) { wil::com_ptr buffer; THROW_IF_FAILED(p.swapChain.swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), reinterpret_cast(buffer.addressof()))); + const auto surface = buffer.query(); const D2D1_RENDER_TARGET_PROPERTIES props{ .type = D2D1_RENDER_TARGET_TYPE_DEFAULT, @@ -83,8 +84,8 @@ void BackendD2D::_handleSettingsUpdate(const RenderingPayload& p) .dpiY = static_cast(p.s->font->dpi), }; // ID2D1RenderTarget and ID2D1DeviceContext are the same and I'm tired of pretending they're not. - THROW_IF_FAILED(p.d2dFactory->CreateDxgiSurfaceRenderTarget(buffer.query().get(), &props, reinterpret_cast(_renderTarget.addressof()))); - _renderTarget.query_to(_renderTarget4.addressof()); + THROW_IF_FAILED(p.d2dFactory->CreateDxgiSurfaceRenderTarget(surface.get(), &props, reinterpret_cast(_renderTarget.addressof()))); + _renderTarget.try_query_to(_renderTarget4.addressof()); _renderTarget->SetUnitMode(D2D1_UNIT_MODE_PIXELS); _renderTarget->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED); diff --git a/src/renderer/atlas/BackendD3D.cpp b/src/renderer/atlas/BackendD3D.cpp index d1990e309c7..056f2f6bbdc 100644 --- a/src/renderer/atlas/BackendD3D.cpp +++ b/src/renderer/atlas/BackendD3D.cpp @@ -693,7 +693,6 @@ void BackendD3D::_resetGlyphAtlas(const RenderingPayload& p) const auto minAreaByFont = cellArea * 95; // Covers all printable ASCII characters const auto minAreaByGrowth = static_cast(_rectPacker.width) * _rectPacker.height * 2; - const auto min = std::max(minArea, std::max(minAreaByFont, minAreaByGrowth)); // It's hard to say what the max. size of the cache should be. Optimally I think we should use as much // memory as is available, but the rendering code in this project is a big mess and so integrating @@ -702,7 +701,9 @@ void BackendD3D::_resetGlyphAtlas(const RenderingPayload& p) // we're locked into a state, where on every render pass we're starting with a half full atlas, drawing once, // filling it with the remaining half and drawing again, requiring two rendering passes on each frame. const auto maxAreaByFont = targetArea + targetArea / 4; - const auto area = std::min(maxArea, std::min(maxAreaByFont, min)); + + auto area = std::min(maxAreaByFont, std::max(minAreaByFont, minAreaByGrowth)); + area = clamp(area, minArea, maxArea); // This block of code calculates the size of a power-of-2 texture that has an area larger than the given `area`. // For instance, for an area of 985x1946 = 1916810 it would result in a u/v of 2048x1024 (area = 2097152). @@ -782,6 +783,7 @@ void BackendD3D::_resizeGlyphAtlas(const RenderingPayload& p, const u16 u, const // We have our own glyph cache so Direct2D's cache doesn't help much. // This saves us 1MB of RAM, which is not much, but also not nothing. + if (_d2dRenderTarget4) { wil::com_ptr device; _d2dRenderTarget4->GetDevice(device.addressof()); @@ -1370,12 +1372,12 @@ bool BackendD3D::_drawGlyph(const RenderingPayload& p, const AtlasFontFaceEntryI static_cast(rect.x + rect.w) / transform.m11, static_cast(rect.y + rect.h) / transform.m22, }; - _d2dRenderTarget4->PushAxisAlignedClip(&clipRect, D2D1_ANTIALIAS_MODE_ALIASED); + _d2dRenderTarget->PushAxisAlignedClip(&clipRect, D2D1_ANTIALIAS_MODE_ALIASED); } const auto boxGlyphCleanup = wil::scope_exit([&]() { if (isBoxGlyph) { - _d2dRenderTarget4->PopAxisAlignedClip(); + _d2dRenderTarget->PopAxisAlignedClip(); } }); diff --git a/src/renderer/atlas/common.h b/src/renderer/atlas/common.h index c0b69b808ec..2b752de8a5e 100644 --- a/src/renderer/atlas/common.h +++ b/src/renderer/atlas/common.h @@ -387,8 +387,8 @@ namespace Microsoft::Console::Render::Atlas til::generational font; til::generational cursor; til::generational misc; - u16x2 targetSize{}; - u16x2 cellCount{}; + u16x2 targetSize{ 1, 1 }; + u16x2 cellCount{ 1, 1 }; }; using GenerationalSettings = til::generational; diff --git a/src/renderer/base/thread.cpp b/src/renderer/base/thread.cpp index 056749f3e4f..904cf2075a2 100644 --- a/src/renderer/base/thread.cpp +++ b/src/renderer/base/thread.cpp @@ -169,6 +169,11 @@ DWORD WINAPI RenderThread::_ThreadProc() { while (_fKeepRunning) { + // Between waiting on _hEvent and calling PaintFrame() there should be a minimal delay, + // so that a key press progresses to a drawing operation as quickly as possible. + // As such, we wait for the renderer to complete _before_ waiting on _hEvent. + _pRenderer->WaitUntilCanRender(); + WaitForSingleObject(_hPaintEnabledEvent, INFINITE); if (!_fNextFrameRequested.exchange(false, std::memory_order_acq_rel)) @@ -209,10 +214,7 @@ DWORD WINAPI RenderThread::_ThreadProc() } ResetEvent(_hPaintCompletedEvent); - - _pRenderer->WaitUntilCanRender(); LOG_IF_FAILED(_pRenderer->PaintFrame()); - SetEvent(_hPaintCompletedEvent); } diff --git a/src/renderer/dx/CustomTextLayout.cpp b/src/renderer/dx/CustomTextLayout.cpp index 695cf04791f..41c64a5a5cd 100644 --- a/src/renderer/dx/CustomTextLayout.cpp +++ b/src/renderer/dx/CustomTextLayout.cpp @@ -406,7 +406,7 @@ CATCH_RETURN() // Get the features to apply to the font const auto& features = _fontRenderData->DefaultFontFeatures(); #pragma warning(suppress : 26492) // Don't use const_cast to cast away const or volatile (type.3). - DWRITE_TYPOGRAPHIC_FEATURES typographicFeatures = { const_cast(features.data()), gsl::narrow(features.size()) }; + const DWRITE_TYPOGRAPHIC_FEATURES typographicFeatures = { const_cast(features.data()), gsl::narrow(features.size()) }; DWRITE_TYPOGRAPHIC_FEATURES const* typographicFeaturesPointer = &typographicFeatures; const uint32_t fontFeatureLengths[] = { textLength }; diff --git a/src/renderer/dx/DxSoftFont.cpp b/src/renderer/dx/DxSoftFont.cpp index f1435644d8e..2bfa932d878 100644 --- a/src/renderer/dx/DxSoftFont.cpp +++ b/src/renderer/dx/DxSoftFont.cpp @@ -23,17 +23,6 @@ constexpr size_t BITMAP_GRID_WIDTH = 12; constexpr size_t BITMAP_GRID_HEIGHT = 8; constexpr size_t PADDING = 2; -constexpr auto ANTIALIASED_INTERPOLATION = D2D1_SCALE_INTERPOLATION_MODE_HIGH_QUALITY_CUBIC; -constexpr auto ALIASED_INTERPOLATION = D2D1_SCALE_INTERPOLATION_MODE_NEAREST_NEIGHBOR; - -DxSoftFont::DxSoftFont() noexcept : - _centeringHint{}, - _interpolation{ ANTIALIASED_INTERPOLATION }, - _colorMatrix{} -{ - _colorMatrix.m[0][3] = 1; -} - void DxSoftFont::SetFont(const std::span bitPattern, const til::size sourceSize, const til::size targetSize, diff --git a/src/renderer/dx/DxSoftFont.h b/src/renderer/dx/DxSoftFont.h index f6c47cc41b2..5dd83b3f4d2 100644 --- a/src/renderer/dx/DxSoftFont.h +++ b/src/renderer/dx/DxSoftFont.h @@ -17,7 +17,6 @@ namespace Microsoft::Console::Render class DxSoftFont { public: - DxSoftFont() noexcept; void SetFont(const std::span bitPattern, const til::size sourceSize, const til::size targetSize, @@ -32,6 +31,9 @@ namespace Microsoft::Console::Render void Reset(); private: + static constexpr auto ANTIALIASED_INTERPOLATION = D2D1_SCALE_INTERPOLATION_MODE_HIGH_QUALITY_CUBIC; + static constexpr auto ALIASED_INTERPOLATION = D2D1_SCALE_INTERPOLATION_MODE_NEAREST_NEIGHBOR; + HRESULT _createResources(gsl::not_null d2dContext); D2D1_VECTOR_2F _scaleForTargetSize() const noexcept; template @@ -39,13 +41,13 @@ namespace Microsoft::Console::Render template T _yOffsetForGlyph(const size_t glyphNumber) const noexcept; - size_t _glyphCount; + size_t _glyphCount = 0; til::size _sourceSize; til::size _targetSize; - size_t _centeringHint; - D2D1_SCALE_INTERPOLATION_MODE _interpolation; - D2D1_MATRIX_5X4_F _colorMatrix; - D2D1_SIZE_U _bitmapSize; + size_t _centeringHint = 0; + D2D1_SCALE_INTERPOLATION_MODE _interpolation = ALIASED_INTERPOLATION; + D2D1_MATRIX_5X4_F _colorMatrix{ ._14 = 1 }; + D2D1_SIZE_U _bitmapSize{}; std::vector _bitmapBits; ::Microsoft::WRL::ComPtr _bitmap; ::Microsoft::WRL::ComPtr _scaleEffect; diff --git a/src/renderer/inc/IRenderData.hpp b/src/renderer/inc/IRenderData.hpp index e3007ab2449..e45dc388ac7 100644 --- a/src/renderer/inc/IRenderData.hpp +++ b/src/renderer/inc/IRenderData.hpp @@ -57,7 +57,7 @@ namespace Microsoft::Console::Render virtual ULONG GetCursorHeight() const noexcept = 0; virtual CursorType GetCursorStyle() const noexcept = 0; virtual ULONG GetCursorPixelWidth() const noexcept = 0; - virtual bool IsCursorDoubleWidth() const noexcept = 0; + virtual bool IsCursorDoubleWidth() const = 0; virtual const std::vector GetOverlays() const noexcept = 0; virtual const bool IsGridLineDrawingAllowed() noexcept = 0; virtual const std::wstring_view GetConsoleTitle() const noexcept = 0; diff --git a/src/renderer/inc/RenderEngineBase.hpp b/src/renderer/inc/RenderEngineBase.hpp index 0091df64466..8c33d59b1e7 100644 --- a/src/renderer/inc/RenderEngineBase.hpp +++ b/src/renderer/inc/RenderEngineBase.hpp @@ -41,7 +41,7 @@ namespace Microsoft::Console::Render const til::CoordType targetRow, const til::CoordType viewportLeft) noexcept override; - [[nodiscard]] virtual bool RequiresContinuousRedraw() noexcept override; + [[nodiscard]] bool RequiresContinuousRedraw() noexcept override; [[nodiscard]] HRESULT InvalidateFlush(_In_ const bool circled, _Out_ bool* const pForcePaint) noexcept override; diff --git a/src/renderer/vt/state.cpp b/src/renderer/vt/state.cpp index e5ddb08514d..29a8657cd5a 100644 --- a/src/renderer/vt/state.cpp +++ b/src/renderer/vt/state.cpp @@ -541,6 +541,9 @@ void VtEngine::SetTerminalCursorTextPosition(const til::point cursor) noexcept // - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. HRESULT VtEngine::RequestWin32Input() noexcept { + // It's important that any additional modes set here are also mirrored in + // the AdaptDispatch::HardReset method, since that needs to re-enable them + // in the connected terminal after passing through an RIS sequence. RETURN_IF_FAILED(_RequestWin32Input()); RETURN_IF_FAILED(_RequestFocusEventMode()); RETURN_IF_FAILED(_Flush()); diff --git a/src/renderer/vt/vtrenderer.hpp b/src/renderer/vt/vtrenderer.hpp index 7f12d12d740..3ba1e702af6 100644 --- a/src/renderer/vt/vtrenderer.hpp +++ b/src/renderer/vt/vtrenderer.hpp @@ -228,7 +228,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT _WriteTerminalAscii(const std::wstring_view str) noexcept; [[nodiscard]] HRESULT _WriteTerminalDrcs(const std::wstring_view str) noexcept; - [[nodiscard]] virtual HRESULT _DoUpdateTitle(const std::wstring_view newTitle) noexcept override; + [[nodiscard]] HRESULT _DoUpdateTitle(const std::wstring_view newTitle) noexcept override; /////////////////////////// Unit Testing Helpers /////////////////////////// #ifdef UNIT_TESTING diff --git a/src/server/ApiDispatchers.cpp b/src/server/ApiDispatchers.cpp index 6b16a2bde05..22f7449a7b8 100644 --- a/src/server/ApiDispatchers.cpp +++ b/src/server/ApiDispatchers.cpp @@ -151,47 +151,15 @@ const auto pInputReadHandleData = pHandleData->GetClientInput(); std::unique_ptr waiter; - HRESULT hr; - std::deque> outEvents; - const auto eventsToRead = cRecords; - if (a->Unicode) - { - if (fIsPeek) - { - hr = m->_pApiRoutines->PeekConsoleInputWImpl(*pInputBuffer, - outEvents, - eventsToRead, - *pInputReadHandleData, - waiter); - } - else - { - hr = m->_pApiRoutines->ReadConsoleInputWImpl(*pInputBuffer, - outEvents, - eventsToRead, - *pInputReadHandleData, - waiter); - } - } - else - { - if (fIsPeek) - { - hr = m->_pApiRoutines->PeekConsoleInputAImpl(*pInputBuffer, - outEvents, - eventsToRead, - *pInputReadHandleData, - waiter); - } - else - { - hr = m->_pApiRoutines->ReadConsoleInputAImpl(*pInputBuffer, - outEvents, - eventsToRead, - *pInputReadHandleData, - waiter); - } - } + InputEventQueue outEvents; + auto hr = m->_pApiRoutines->GetConsoleInputImpl( + *pInputBuffer, + outEvents, + cRecords, + *pInputReadHandleData, + a->Unicode, + fIsPeek, + waiter); // We must return the number of records in the message payload (to alert the client) // as well as in the message headers (below in SetReplyInformation) to alert the driver. diff --git a/src/server/IApiRoutines.h b/src/server/IApiRoutines.h index 9d9307d10ab..a5d691bf149 100644 --- a/src/server/IApiRoutines.h +++ b/src/server/IApiRoutines.h @@ -66,29 +66,13 @@ class IApiRoutines [[nodiscard]] virtual HRESULT GetNumberOfConsoleInputEventsImpl(const IConsoleInputObject& context, ULONG& events) noexcept = 0; - [[nodiscard]] virtual HRESULT PeekConsoleInputAImpl(IConsoleInputObject& context, - std::deque>& outEvents, - const size_t eventsToRead, - INPUT_READ_HANDLE_DATA& readHandleState, - std::unique_ptr& waiter) noexcept = 0; - - [[nodiscard]] virtual HRESULT PeekConsoleInputWImpl(IConsoleInputObject& context, - std::deque>& outEvents, - const size_t eventsToRead, - INPUT_READ_HANDLE_DATA& readHandleState, - std::unique_ptr& waiter) noexcept = 0; - - [[nodiscard]] virtual HRESULT ReadConsoleInputAImpl(IConsoleInputObject& context, - std::deque>& outEvents, - const size_t eventsToRead, - INPUT_READ_HANDLE_DATA& readHandleState, - std::unique_ptr& waiter) noexcept = 0; - - [[nodiscard]] virtual HRESULT ReadConsoleInputWImpl(IConsoleInputObject& context, - std::deque>& outEvents, - const size_t eventsToRead, - INPUT_READ_HANDLE_DATA& readHandleState, - std::unique_ptr& waiter) noexcept = 0; + [[nodiscard]] virtual 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 = 0; [[nodiscard]] virtual HRESULT ReadConsoleAImpl(IConsoleInputObject& context, std::span buffer, diff --git a/src/server/IWaitRoutine.h b/src/server/IWaitRoutine.h index b73ea768c06..59ffa905bd8 100644 --- a/src/server/IWaitRoutine.h +++ b/src/server/IWaitRoutine.h @@ -37,6 +37,11 @@ class IWaitRoutine virtual ~IWaitRoutine() = default; + IWaitRoutine(const IWaitRoutine&) = delete; + IWaitRoutine(IWaitRoutine&&) = delete; + IWaitRoutine& operator=(const IWaitRoutine&) & = delete; + IWaitRoutine& operator=(IWaitRoutine&&) & = delete; + virtual void MigrateUserBuffersOnTransitionToBackgroundWait(const void* oldBuffer, void* newBuffer) = 0; virtual bool Notify(const WaitTerminationReason TerminationReason, diff --git a/src/terminal/adapter/DispatchTypes.hpp b/src/terminal/adapter/DispatchTypes.hpp index f123d948428..9e342ec5f80 100644 --- a/src/terminal/adapter/DispatchTypes.hpp +++ b/src/terminal/adapter/DispatchTypes.hpp @@ -432,6 +432,7 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes DECNKM_NumericKeypadMode = DECPrivateMode(66), DECBKM_BackarrowKeyMode = DECPrivateMode(67), DECLRMM_LeftRightMarginMode = DECPrivateMode(69), + DECECM_EraseColorMode = DECPrivateMode(117), VT200_MOUSE_MODE = DECPrivateMode(1000), BUTTON_EVENT_MOUSE_MODE = DECPrivateMode(1002), ANY_EVENT_MOUSE_MODE = DECPrivateMode(1003), diff --git a/src/terminal/adapter/ITerminalApi.hpp b/src/terminal/adapter/ITerminalApi.hpp index a3bcaadfb8b..5d9cb97d3d0 100644 --- a/src/terminal/adapter/ITerminalApi.hpp +++ b/src/terminal/adapter/ITerminalApi.hpp @@ -60,7 +60,7 @@ namespace Microsoft::Console::VirtualTerminal virtual void WarningBell() = 0; virtual void SetWindowTitle(const std::wstring_view title) = 0; - virtual void UseAlternateScreenBuffer() = 0; + virtual void UseAlternateScreenBuffer(const TextAttribute& attrs) = 0; virtual void UseMainScreenBuffer() = 0; virtual CursorType GetUserDefaultCursorStyle() const = 0; diff --git a/src/terminal/adapter/InteractDispatch.cpp b/src/terminal/adapter/InteractDispatch.cpp index bf1951c88b0..1dc9f446b54 100644 --- a/src/terminal/adapter/InteractDispatch.cpp +++ b/src/terminal/adapter/InteractDispatch.cpp @@ -240,7 +240,7 @@ bool InteractDispatch::FocusChanged(const bool focused) const WI_UpdateFlag(gci.Flags, CONSOLE_HAS_FOCUS, shouldActuallyFocus); gci.ProcessHandleList.ModifyConsoleProcessFocus(shouldActuallyFocus); - gci.pInputBuffer->Write(std::make_unique(focused)); + gci.pInputBuffer->WriteFocusEvent(focused); } // Does nothing outside of ConPTY. If there's a real HWND, then the HWND is solely in charge. diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index 95a6a7092df..36b95b12514 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -15,6 +15,8 @@ using namespace Microsoft::Console::Types; using namespace Microsoft::Console::Render; using namespace Microsoft::Console::VirtualTerminal; +static constexpr std::wstring_view whitespace{ L" " }; + AdaptDispatch::AdaptDispatch(ITerminalApi& api, Renderer& renderer, RenderSettings& renderSettings, TerminalInput& terminalInput) : _api{ api }, _renderer{ renderer }, @@ -136,9 +138,16 @@ void AdaptDispatch::_WriteToBuffer(const std::wstring_view string) state.columnBegin = cursorPosition.x; const auto textPositionBefore = state.text.data(); - textBuffer.WriteLine(cursorPosition.y, wrapAtEOL, attributes, state); + textBuffer.Write(cursorPosition.y, attributes, state); const auto textPositionAfter = state.text.data(); + // TODO: A row should not be marked as wrapped just because we wrote the last column. + // It should be marked whenever we write _past_ it (above, _DoLineFeed call). See GH#15602. + if (wrapAtEOL && state.columnEnd >= state.columnLimit) + { + textBuffer.SetWrapForced(cursorPosition.y, true); + } + if (state.columnBeginDirty != state.columnEndDirty) { const til::rect changedRect{ state.columnBeginDirty, cursorPosition.y, state.columnEndDirty, cursorPosition.y + 1 }; @@ -563,6 +572,28 @@ bool AdaptDispatch::CursorRestoreState() return true; } +// Routine Description: +// - Returns the attributes that should be used when erasing the buffer. When +// the Erase Color mode is set, we use the default attributes, but when reset, +// we use the active color attributes with the character attributes cleared. +// Arguments: +// - textBuffer - Target buffer that is being erased. +// Return Value: +// - The erase TextAttribute value. +TextAttribute AdaptDispatch::_GetEraseAttributes(const TextBuffer& textBuffer) const noexcept +{ + if (_modes.test(Mode::EraseColor)) + { + return {}; + } + else + { + auto eraseAttributes = textBuffer.GetCurrentAttributes(); + eraseAttributes.SetStandardErase(); + return eraseAttributes; + } +} + // Routine Description: // - Scrolls an area of the buffer in a vertical direction. // Arguments: @@ -611,9 +642,8 @@ void AdaptDispatch::_ScrollRectVertically(TextBuffer& textBuffer, const til::rec auto eraseRect = scrollRect; eraseRect.top = delta > 0 ? scrollRect.top : (scrollRect.bottom - absoluteDelta); eraseRect.bottom = eraseRect.top + absoluteDelta; - auto eraseAttributes = textBuffer.GetCurrentAttributes(); - eraseAttributes.SetStandardErase(); - _FillRect(textBuffer, eraseRect, L' ', eraseAttributes); + const auto eraseAttributes = _GetEraseAttributes(textBuffer); + _FillRect(textBuffer, eraseRect, whitespace, eraseAttributes); // Also reset the line rendition for the erased rows. textBuffer.ResetLineRenditionRange(eraseRect.top, eraseRect.bottom); @@ -660,9 +690,8 @@ void AdaptDispatch::_ScrollRectHorizontally(TextBuffer& textBuffer, const til::r auto eraseRect = scrollRect; eraseRect.left = delta > 0 ? scrollRect.left : (scrollRect.right - absoluteDelta); eraseRect.right = eraseRect.left + absoluteDelta; - auto eraseAttributes = textBuffer.GetCurrentAttributes(); - eraseAttributes.SetStandardErase(); - _FillRect(textBuffer, eraseRect, L' ', eraseAttributes); + const auto eraseAttributes = _GetEraseAttributes(textBuffer); + _FillRect(textBuffer, eraseRect, whitespace, eraseAttributes); } // Routine Description: @@ -726,19 +755,10 @@ bool AdaptDispatch::DeleteCharacter(const VTInt count) // - fillAttrs - Attributes to be written to the buffer. // Return Value: // - -void AdaptDispatch::_FillRect(TextBuffer& textBuffer, const til::rect& fillRect, const wchar_t fillChar, const TextAttribute fillAttrs) +void AdaptDispatch::_FillRect(TextBuffer& textBuffer, const til::rect& fillRect, const std::wstring_view& fillChar, const TextAttribute& fillAttrs) const { - if (fillRect.left < fillRect.right && fillRect.top < fillRect.bottom) - { - const auto fillWidth = gsl::narrow_cast(fillRect.right - fillRect.left); - const auto fillData = OutputCellIterator{ fillChar, fillAttrs, fillWidth }; - const auto col = fillRect.left; - for (auto row = fillRect.top; row < fillRect.bottom; row++) - { - textBuffer.WriteLine(fillData, { col, row }, false); - } - _api.NotifyAccessibilityChange(fillRect); - } + textBuffer.FillRect(fillRect, fillChar, fillAttrs); + _api.NotifyAccessibilityChange(fillRect); } // Routine Description: @@ -760,9 +780,8 @@ bool AdaptDispatch::EraseCharacters(const VTInt numChars) // The ECH control is expected to reset the delayed wrap flag. textBuffer.GetCursor().ResetDelayEOLWrap(); - auto eraseAttributes = textBuffer.GetCurrentAttributes(); - eraseAttributes.SetStandardErase(); - _FillRect(textBuffer, { startCol, row, endCol, row + 1 }, L' ', eraseAttributes); + const auto eraseAttributes = _GetEraseAttributes(textBuffer); + _FillRect(textBuffer, { startCol, row, endCol, row + 1 }, whitespace, eraseAttributes); return true; } @@ -806,8 +825,7 @@ bool AdaptDispatch::EraseInDisplay(const DispatchTypes::EraseType eraseType) // take care of that themselves when they set the cursor position. textBuffer.GetCursor().ResetDelayEOLWrap(); - auto eraseAttributes = textBuffer.GetCurrentAttributes(); - eraseAttributes.SetStandardErase(); + const auto eraseAttributes = _GetEraseAttributes(textBuffer); // When erasing the display, every line that is erased in full should be // reset to single width. When erasing to the end, this could include @@ -818,14 +836,14 @@ bool AdaptDispatch::EraseInDisplay(const DispatchTypes::EraseType eraseType) if (eraseType == DispatchTypes::EraseType::FromBeginning) { textBuffer.ResetLineRenditionRange(viewport.top, row); - _FillRect(textBuffer, { 0, viewport.top, bufferWidth, row }, L' ', eraseAttributes); - _FillRect(textBuffer, { 0, row, col + 1, row + 1 }, L' ', eraseAttributes); + _FillRect(textBuffer, { 0, viewport.top, bufferWidth, row }, whitespace, eraseAttributes); + _FillRect(textBuffer, { 0, row, col + 1, row + 1 }, whitespace, eraseAttributes); } if (eraseType == DispatchTypes::EraseType::ToEnd) { textBuffer.ResetLineRenditionRange(col > 0 ? row + 1 : row, viewport.bottom); - _FillRect(textBuffer, { col, row, bufferWidth, row + 1 }, L' ', eraseAttributes); - _FillRect(textBuffer, { 0, row + 1, bufferWidth, viewport.bottom }, L' ', eraseAttributes); + _FillRect(textBuffer, { col, row, bufferWidth, row + 1 }, whitespace, eraseAttributes); + _FillRect(textBuffer, { 0, row + 1, bufferWidth, viewport.bottom }, whitespace, eraseAttributes); } return true; @@ -846,18 +864,17 @@ bool AdaptDispatch::EraseInLine(const DispatchTypes::EraseType eraseType) // The EL control is expected to reset the delayed wrap flag. textBuffer.GetCursor().ResetDelayEOLWrap(); - auto eraseAttributes = textBuffer.GetCurrentAttributes(); - eraseAttributes.SetStandardErase(); + const auto eraseAttributes = _GetEraseAttributes(textBuffer); switch (eraseType) { case DispatchTypes::EraseType::FromBeginning: - _FillRect(textBuffer, { 0, row, col + 1, row + 1 }, L' ', eraseAttributes); + _FillRect(textBuffer, { 0, row, col + 1, row + 1 }, whitespace, eraseAttributes); return true; case DispatchTypes::EraseType::ToEnd: - _FillRect(textBuffer, { col, row, textBuffer.GetLineWidth(row), row + 1 }, L' ', eraseAttributes); + _FillRect(textBuffer, { col, row, textBuffer.GetLineWidth(row), row + 1 }, whitespace, eraseAttributes); return true; case DispatchTypes::EraseType::All: - _FillRect(textBuffer, { 0, row, textBuffer.GetLineWidth(row), row + 1 }, L' ', eraseAttributes); + _FillRect(textBuffer, { 0, row, textBuffer.GetLineWidth(row), row + 1 }, whitespace, eraseAttributes); return true; default: return false; @@ -1253,7 +1270,7 @@ bool AdaptDispatch::CopyRectangularArea(const VTInt top, const VTInt left, const bool AdaptDispatch::FillRectangularArea(const VTParameter ch, const VTInt top, const VTInt left, const VTInt bottom, const VTInt right) { auto& textBuffer = _api.GetTextBuffer(); - auto fillRect = _CalculateRectArea(top, left, bottom, right, textBuffer.GetSize().Dimensions()); + const auto fillRect = _CalculateRectArea(top, left, bottom, right, textBuffer.GetSize().Dimensions()); // The standard only allows for characters in the range of the GL and GR // character set tables, but we also support additional Unicode characters @@ -1265,14 +1282,8 @@ bool AdaptDispatch::FillRectangularArea(const VTParameter ch, const VTInt top, c if (glChar || grChar || unicodeChar) { const auto fillChar = _termOutput.TranslateKey(gsl::narrow_cast(charValue)); - const auto fillAttributes = textBuffer.GetCurrentAttributes(); - if (IsGlyphFullWidth(fillChar)) - { - // If the fill char is full width, we need to halve the width of the - // fill area, otherwise it'll occupy twice as much space as expected. - fillRect.right = fillRect.left + fillRect.width() / 2; - } - _FillRect(textBuffer, fillRect, fillChar, fillAttributes); + const auto& fillAttributes = textBuffer.GetCurrentAttributes(); + _FillRect(textBuffer, fillRect, { &fillChar, 1 }, fillAttributes); } return true; @@ -1292,9 +1303,8 @@ bool AdaptDispatch::EraseRectangularArea(const VTInt top, const VTInt left, cons { auto& textBuffer = _api.GetTextBuffer(); const auto eraseRect = _CalculateRectArea(top, left, bottom, right, textBuffer.GetSize().Dimensions()); - auto eraseAttributes = textBuffer.GetCurrentAttributes(); - eraseAttributes.SetStandardErase(); - _FillRect(textBuffer, eraseRect, L' ', eraseAttributes); + const auto eraseAttributes = _GetEraseAttributes(textBuffer); + _FillRect(textBuffer, eraseRect, whitespace, eraseAttributes); return true; } @@ -1430,7 +1440,8 @@ bool AdaptDispatch::SetLineRendition(const LineRendition rendition) if (!_modes.test(Mode::AllowDECSLRM)) { auto& textBuffer = _api.GetTextBuffer(); - textBuffer.SetCurrentLineRendition(rendition); + const auto eraseAttributes = _GetEraseAttributes(textBuffer); + textBuffer.SetCurrentLineRendition(rendition, eraseAttributes); // There is some variation in how this was handled by the different DEC // terminals, but the STD 070 reference (on page D-13) makes it clear that // the delayed wrap (aka the Last Column Flag) was expected to be reset when @@ -1748,7 +1759,8 @@ void AdaptDispatch::_SetAlternateScreenBufferMode(const bool enable) if (enable) { CursorSaveState(); - _api.UseAlternateScreenBuffer(); + const auto& textBuffer = _api.GetTextBuffer(); + _api.UseAlternateScreenBuffer(_GetEraseAttributes(textBuffer)); _usingAltBuffer = true; } else @@ -1858,6 +1870,9 @@ bool AdaptDispatch::_ModeParamsHelper(const DispatchTypes::ModeParams param, con textBuffer.ResetLineRenditionRange(viewport.top, viewport.bottom); } return true; + case DispatchTypes::ModeParams::DECECM_EraseColorMode: + _modes.set(Mode::EraseColor, enable); + return true; case DispatchTypes::ModeParams::VT200_MOUSE_MODE: _terminalInput.SetInputMode(TerminalInput::Mode::DefaultMouseTracking, enable); return !_PassThroughInputModes(); @@ -1989,6 +2004,9 @@ bool AdaptDispatch::RequestMode(const DispatchTypes::ModeParams param) case DispatchTypes::ModeParams::DECLRMM_LeftRightMarginMode: enabled = _modes.test(Mode::AllowDECSLRM); break; + case DispatchTypes::ModeParams::DECECM_EraseColorMode: + enabled = _modes.test(Mode::EraseColor); + break; case DispatchTypes::ModeParams::VT200_MOUSE_MODE: enabled = _terminalInput.GetInputMode(TerminalInput::Mode::DefaultMouseTracking); break; @@ -2427,15 +2445,14 @@ void AdaptDispatch::_DoLineFeed(TextBuffer& textBuffer, const bool withReturn, c // And if the bottom margin didn't cover the full viewport, we copy the // lower part of the viewport down so it remains static. But for a full - // pan we reset the newly revealed row with the current attributes. + // pan we reset the newly revealed row with the erase attributes. if (bottomMargin < viewport.bottom - 1) { _ScrollRectVertically(textBuffer, { 0, bottomMargin + 1, bufferWidth, viewport.bottom + 1 }, 1); } else { - auto eraseAttributes = textBuffer.GetCurrentAttributes(); - eraseAttributes.SetStandardErase(); + const auto eraseAttributes = _GetEraseAttributes(textBuffer); textBuffer.GetRowByOffset(newPosition.y).Reset(eraseAttributes); } } @@ -2444,7 +2461,8 @@ void AdaptDispatch::_DoLineFeed(TextBuffer& textBuffer, const bool withReturn, c // If the viewport has reached the end of the buffer, we can't pan down, // so we cycle the row coordinates, which effectively scrolls the buffer // content up. In this case we don't need to move the cursor down. - textBuffer.IncrementCircularBuffer(true); + const auto eraseAttributes = _GetEraseAttributes(textBuffer); + textBuffer.IncrementCircularBuffer(eraseAttributes); _api.NotifyBufferRotation(1); // We trigger a scroll rather than a redraw, since that's more efficient, @@ -3055,11 +3073,21 @@ bool AdaptDispatch::HardReset() _macroBuffer = nullptr; } - // GH#2715 - If all this succeeded, but we're in a conpty, return `false` to - // make the state machine propagate this RIS sequence to the connected - // terminal application. We've reset our state, but the connected terminal - // might need to do more. - return !_api.IsConsolePty(); + // If we're in a conpty, we need flush this RIS sequence to the connected + // terminal application, but we also need to follow that up with a DECSET + // sequence to re-enable the modes that we require (namely win32 input mode + // and focus event mode). It's important that this is kept in sync with the + // VtEngine::RequestWin32Input method which requests the modes on startup. + if (_api.IsConsolePty()) + { + auto& stateMachine = _api.GetStateMachine(); + if (stateMachine.FlushToTerminal()) + { + auto& engine = stateMachine.Engine(); + engine.ActionPassThroughString(L"\033[?9001;1004h"); + } + } + return true; } // Routine Description: @@ -3077,7 +3105,7 @@ bool AdaptDispatch::ScreenAlignmentPattern() const auto bufferWidth = textBuffer.GetSize().Dimensions().width; // Fill the screen with the letter E using the default attributes. - _FillRect(textBuffer, { 0, viewport.top, bufferWidth, viewport.bottom }, L'E', {}); + _FillRect(textBuffer, { 0, viewport.top, bufferWidth, viewport.bottom }, L"E", {}); // Reset the line rendition for all of these rows. textBuffer.ResetLineRenditionRange(viewport.top, viewport.bottom); // Reset the meta/extended attributes (but leave the colors unchanged). @@ -3120,7 +3148,7 @@ bool AdaptDispatch::_EraseScrollback() // Scroll the viewport content to the top of the buffer. textBuffer.ScrollRows(top, height, -top); // Clear everything after the viewport. - _FillRect(textBuffer, { 0, height, bufferSize.width, bufferSize.height }, L' ', {}); + _FillRect(textBuffer, { 0, height, bufferSize.width, bufferSize.height }, whitespace, {}); // Also reset the line rendition for all of the cleared rows. textBuffer.ResetLineRenditionRange(height, bufferSize.height); // Move the viewport @@ -3167,7 +3195,7 @@ bool AdaptDispatch::_EraseAll() // start of the buffer, then we shouldn't move down at all. const auto lastChar = textBuffer.GetLastNonSpaceCharacter(); auto newViewportTop = lastChar == til::point{} ? 0 : lastChar.y + 1; - const auto newViewportBottom = newViewportTop + viewportHeight; + auto newViewportBottom = newViewportTop + viewportHeight; const auto delta = newViewportBottom - (bufferSize.Height()); if (delta > 0) { @@ -3177,6 +3205,7 @@ bool AdaptDispatch::_EraseAll() } _api.NotifyBufferRotation(delta); newViewportTop -= delta; + newViewportBottom -= delta; // We don't want to trigger a scroll in pty mode, because we're going to // pass through the ED sequence anyway, and this will just result in the // buffer being scrolled up by two pages instead of one. @@ -3192,9 +3221,8 @@ bool AdaptDispatch::_EraseAll() cursor.SetHasMoved(true); // Erase all the rows in the current viewport. - auto eraseAttributes = textBuffer.GetCurrentAttributes(); - eraseAttributes.SetStandardErase(); - _FillRect(textBuffer, { 0, newViewportTop, bufferSize.Width(), newViewportBottom }, L' ', eraseAttributes); + const auto eraseAttributes = _GetEraseAttributes(textBuffer); + _FillRect(textBuffer, { 0, newViewportTop, bufferSize.Width(), newViewportBottom }, whitespace, eraseAttributes); // Also reset the line rendition for the erased rows. textBuffer.ResetLineRenditionRange(newViewportTop, newViewportBottom); diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp index 5d5795a892c..61f841371aa 100644 --- a/src/terminal/adapter/adaptDispatch.hpp +++ b/src/terminal/adapter/adaptDispatch.hpp @@ -172,6 +172,7 @@ namespace Microsoft::Console::VirtualTerminal Column, AllowDECCOLM, AllowDECSLRM, + EraseColor, RectangularChangeExtent }; enum class ScrollDirection @@ -213,13 +214,14 @@ namespace Microsoft::Console::VirtualTerminal std::pair _GetHorizontalMargins(const til::CoordType bufferWidth) noexcept; bool _CursorMovePosition(const Offset rowOffset, const Offset colOffset, const bool clampInMargins); void _ApplyCursorMovementFlags(Cursor& cursor) noexcept; - void _FillRect(TextBuffer& textBuffer, const til::rect& fillRect, const wchar_t fillChar, const TextAttribute fillAttrs); + void _FillRect(TextBuffer& textBuffer, const til::rect& fillRect, const std::wstring_view& fillChar, const TextAttribute& fillAttrs) const; void _SelectiveEraseRect(TextBuffer& textBuffer, const til::rect& eraseRect); void _ChangeRectAttributes(TextBuffer& textBuffer, const til::rect& changeRect, const ChangeOps& changeOps); void _ChangeRectOrStreamAttributes(const til::rect& changeArea, const ChangeOps& changeOps); til::rect _CalculateRectArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right, const til::size bufferSize); bool _EraseScrollback(); bool _EraseAll(); + TextAttribute _GetEraseAttributes(const TextBuffer& textBuffer) const noexcept; void _ScrollRectVertically(TextBuffer& textBuffer, const til::rect& scrollRect, const VTInt delta); void _ScrollRectHorizontally(TextBuffer& textBuffer, const til::rect& scrollRect, const VTInt delta); void _InsertDeleteCharacterHelper(const VTInt delta); diff --git a/src/terminal/adapter/ut_adapter/MouseInputTest.cpp b/src/terminal/adapter/ut_adapter/MouseInputTest.cpp index 6c22709de12..9995d15d107 100644 --- a/src/terminal/adapter/ut_adapter/MouseInputTest.cpp +++ b/src/terminal/adapter/ut_adapter/MouseInputTest.cpp @@ -23,12 +23,6 @@ namespace Microsoft }; using namespace Microsoft::Console::VirtualTerminal; -// For magic reasons, this has to live outside the class. Something wonderful about TAEF macros makes it -// invisible to the linker when inside the class. -static const wchar_t* s_pwszInputExpected; - -static wchar_t s_pwszExpectedBuffer[BYTE_MAX]; // big enough for anything - static til::point s_rgTestCoords[] = { { 0, 0 }, { 0, 1 }, @@ -85,71 +79,29 @@ class MouseInputTest public: TEST_CLASS(MouseInputTest); - static void s_MouseInputTestCallback(_Inout_ std::deque>& events) - { - Log::Comment(L"MouseInput successfully generated a sequence for the input, and sent it."); - - size_t cInputExpected = 0; - VERIFY_SUCCEEDED(StringCchLengthW(s_pwszInputExpected, STRSAFE_MAX_CCH, &cInputExpected)); - - if (VERIFY_ARE_EQUAL(cInputExpected, events.size(), L"Verify expected and actual input array lengths matched.")) - { - Log::Comment(L"We are expecting always key events and always key down. All other properties should not be written by simulated keys."); - - for (size_t i = 0; i < events.size(); ++i) - { - KeyEvent expectedKeyEvent(TRUE, 1, 0, 0, s_pwszInputExpected[i], 0); - auto testKeyEvent = *static_cast(events[i].get()); - VERIFY_ARE_EQUAL(expectedKeyEvent, testKeyEvent, NoThrowString().Format(L"Chars='%c','%c'", s_pwszInputExpected[i], testKeyEvent.GetCharData())); - } - } - } - - void ClearTestBuffer() - { - memset(s_pwszExpectedBuffer, 0, ARRAYSIZE(s_pwszExpectedBuffer) * sizeof(wchar_t)); - } - // Routine Description: // Constructs a string from s_rgDefaultTestOutput with the third char // correctly filled in to match uiButton. - wchar_t* BuildDefaultTestOutput(const wchar_t* pwchTestOutput, unsigned int uiButton, short sModifierKeystate, short sScrollDelta) + TerminalInput::OutputType BuildDefaultTestOutput(const std::wstring_view& pwchTestOutput, unsigned int uiButton, short sModifierKeystate, short sScrollDelta) { - Log::Comment(NoThrowString().Format(L"Input Test Output:\'%s\'", pwchTestOutput)); - // Copy the expected output into the buffer - size_t cchInputExpected = 0; - VERIFY_SUCCEEDED(StringCchLengthW(pwchTestOutput, STRSAFE_MAX_CCH, &cchInputExpected)); - VERIFY_ARE_EQUAL(cchInputExpected, 6ul); - - ClearTestBuffer(); - memcpy(s_pwszExpectedBuffer, pwchTestOutput, cchInputExpected * sizeof(wchar_t)); - - // Change the expected button value - auto wch = GetDefaultCharFromButton(uiButton, sModifierKeystate, sScrollDelta); - Log::Comment(NoThrowString().Format(L"Button Char was:\'%d\' for uiButton '%d", (int)wch, uiButton)); - - s_pwszExpectedBuffer[3] = wch; - Log::Comment(NoThrowString().Format(L"Expected Input:\'%s\'", s_pwszExpectedBuffer)); - return s_pwszExpectedBuffer; + TerminalInput::StringType str; + str.append(pwchTestOutput); + str[3] = GetDefaultCharFromButton(uiButton, sModifierKeystate, sScrollDelta); + return str; } // Routine Description: // Constructs a string from s_rgSgrTestOutput with the third and last chars // correctly filled in to match uiButton. - wchar_t* BuildSGRTestOutput(const wchar_t* pwchTestOutput, unsigned int uiButton, short sModifierKeystate, short sScrollDelta) + TerminalInput::OutputType BuildSGRTestOutput(const wchar_t* pwchTestOutput, unsigned int uiButton, short sModifierKeystate, short sScrollDelta) { - ClearTestBuffer(); - - // Copy the expected output into the buffer - swprintf_s(s_pwszExpectedBuffer, BYTE_MAX, pwchTestOutput, GetSgrCharFromButton(uiButton, sModifierKeystate, sScrollDelta)); + wchar_t buffer[256]; + swprintf_s(buffer, pwchTestOutput, GetSgrCharFromButton(uiButton, sModifierKeystate, sScrollDelta)); - size_t cchInputExpected = 0; - VERIFY_SUCCEEDED(StringCchLengthW(s_pwszExpectedBuffer, STRSAFE_MAX_CCH, &cchInputExpected)); - - s_pwszExpectedBuffer[cchInputExpected - 1] = IsButtonDown(uiButton) ? L'M' : L'm'; - - Log::Comment(NoThrowString().Format(L"Expected Input:\'%s\'", s_pwszExpectedBuffer)); - return s_pwszExpectedBuffer; + TerminalInput::StringType str; + str.append(std::wstring_view{ buffer }); + str[str.size() - 1] = IsButtonDown(uiButton) ? L'M' : L'm'; + return str; } wchar_t GetDefaultCharFromButton(unsigned int uiButton, short sModifierKeystate, short sScrollDelta) @@ -284,7 +236,7 @@ class MouseInputTest Log::Comment(L"Starting test..."); - auto mouseInput = std::make_unique(s_MouseInputTestCallback); + TerminalInput mouseInput; unsigned int uiModifierKeystate = 0; VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiModifierKeystate", uiModifierKeystate)); @@ -294,65 +246,69 @@ class MouseInputTest unsigned int uiButton; VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiButton", uiButton)); - auto fExpectedKeyHandled = false; - s_pwszInputExpected = L"\x0"; - VERIFY_ARE_EQUAL(fExpectedKeyHandled, mouseInput->HandleMouse({ 0, 0 }, uiButton, sModifierKeystate, sScrollDelta, {})); + VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), mouseInput.HandleMouse({ 0, 0 }, uiButton, sModifierKeystate, sScrollDelta, {})); - mouseInput->SetInputMode(TerminalInput::Mode::DefaultMouseTracking, true); + mouseInput.SetInputMode(TerminalInput::Mode::DefaultMouseTracking, true); for (auto i = 0; i < s_iTestCoordsLength; i++) { - auto Coord = s_rgTestCoords[i]; - fExpectedKeyHandled = (Coord.x <= 94 && Coord.y <= 94); - Log::Comment(NoThrowString().Format(L"fHandled, x, y = (%d, %d, %d)", fExpectedKeyHandled, Coord.x, Coord.y)); + const auto Coord = s_rgTestCoords[i]; - s_pwszInputExpected = BuildDefaultTestOutput(s_rgDefaultTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + TerminalInput::OutputType expected; + if (Coord.x <= 94 && Coord.y <= 94) + { + expected = BuildDefaultTestOutput(s_rgDefaultTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + } // validate translation - VERIFY_ARE_EQUAL(fExpectedKeyHandled, - mouseInput->HandleMouse(Coord, - uiButton, - sModifierKeystate, - sScrollDelta, - {}), + VERIFY_ARE_EQUAL(expected, + mouseInput.HandleMouse(Coord, + uiButton, + sModifierKeystate, + sScrollDelta, + {}), NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.x, Coord.y)); } - mouseInput->SetInputMode(TerminalInput::Mode::ButtonEventMouseTracking, true); + mouseInput.SetInputMode(TerminalInput::Mode::ButtonEventMouseTracking, true); for (auto i = 0; i < s_iTestCoordsLength; i++) { - auto Coord = s_rgTestCoords[i]; - fExpectedKeyHandled = (Coord.x <= 94 && Coord.y <= 94); - Log::Comment(NoThrowString().Format(L"fHandled, x, y = (%d, %d, %d)", fExpectedKeyHandled, Coord.x, Coord.y)); + const auto Coord = s_rgTestCoords[i]; - s_pwszInputExpected = BuildDefaultTestOutput(s_rgDefaultTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + TerminalInput::OutputType expected; + if (Coord.x <= 94 && Coord.y <= 94) + { + expected = BuildDefaultTestOutput(s_rgDefaultTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + } // validate translation - VERIFY_ARE_EQUAL(fExpectedKeyHandled, - mouseInput->HandleMouse(Coord, - uiButton, - sModifierKeystate, - sScrollDelta, - {}), + VERIFY_ARE_EQUAL(expected, + mouseInput.HandleMouse(Coord, + uiButton, + sModifierKeystate, + sScrollDelta, + {}), NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.x, Coord.y)); } - mouseInput->SetInputMode(TerminalInput::Mode::AnyEventMouseTracking, true); + mouseInput.SetInputMode(TerminalInput::Mode::AnyEventMouseTracking, true); for (auto i = 0; i < s_iTestCoordsLength; i++) { - auto Coord = s_rgTestCoords[i]; - fExpectedKeyHandled = (Coord.x <= 94 && Coord.y <= 94); - Log::Comment(NoThrowString().Format(L"fHandled, x, y = (%d, %d, %d)", fExpectedKeyHandled, Coord.x, Coord.y)); + const auto Coord = s_rgTestCoords[i]; - s_pwszInputExpected = BuildDefaultTestOutput(s_rgDefaultTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + TerminalInput::OutputType expected; + if (Coord.x <= 94 && Coord.y <= 94) + { + expected = BuildDefaultTestOutput(s_rgDefaultTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + } // validate translation - VERIFY_ARE_EQUAL(fExpectedKeyHandled, - mouseInput->HandleMouse(Coord, - uiButton, - sModifierKeystate, - sScrollDelta, - {}), + VERIFY_ARE_EQUAL(expected, + mouseInput.HandleMouse(Coord, + uiButton, + sModifierKeystate, + sScrollDelta, + {}), NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.x, Coord.y)); } } @@ -367,7 +323,7 @@ class MouseInputTest Log::Comment(L"Starting test..."); - auto mouseInput = std::make_unique(s_MouseInputTestCallback); + TerminalInput mouseInput; unsigned int uiModifierKeystate = 0; VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiModifierKeystate", uiModifierKeystate)); @@ -377,68 +333,72 @@ class MouseInputTest unsigned int uiButton; VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiButton", uiButton)); - auto fExpectedKeyHandled = false; - s_pwszInputExpected = L"\x0"; - VERIFY_ARE_EQUAL(fExpectedKeyHandled, mouseInput->HandleMouse({ 0, 0 }, uiButton, sModifierKeystate, sScrollDelta, {})); + VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), mouseInput.HandleMouse({ 0, 0 }, uiButton, sModifierKeystate, sScrollDelta, {})); - mouseInput->SetInputMode(TerminalInput::Mode::Utf8MouseEncoding, true); + mouseInput.SetInputMode(TerminalInput::Mode::Utf8MouseEncoding, true); auto MaxCoord = SHORT_MAX - 33; - mouseInput->SetInputMode(TerminalInput::Mode::DefaultMouseTracking, true); + mouseInput.SetInputMode(TerminalInput::Mode::DefaultMouseTracking, true); for (auto i = 0; i < s_iTestCoordsLength; i++) { - auto Coord = s_rgTestCoords[i]; + const auto Coord = s_rgTestCoords[i]; - fExpectedKeyHandled = (Coord.x <= MaxCoord && Coord.y <= MaxCoord); - Log::Comment(NoThrowString().Format(L"fHandled, x, y = (%d, %d, %d)", fExpectedKeyHandled, Coord.x, Coord.y)); - s_pwszInputExpected = BuildDefaultTestOutput(s_rgDefaultTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + TerminalInput::OutputType expected; + if (Coord.x <= MaxCoord && Coord.y <= MaxCoord) + { + expected = BuildDefaultTestOutput(s_rgDefaultTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + } // validate translation - VERIFY_ARE_EQUAL(fExpectedKeyHandled, - mouseInput->HandleMouse(Coord, - uiButton, - sModifierKeystate, - sScrollDelta, - {}), + VERIFY_ARE_EQUAL(expected, + mouseInput.HandleMouse(Coord, + uiButton, + sModifierKeystate, + sScrollDelta, + {}), NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.x, Coord.y)); } - mouseInput->SetInputMode(TerminalInput::Mode::ButtonEventMouseTracking, true); + mouseInput.SetInputMode(TerminalInput::Mode::ButtonEventMouseTracking, true); for (auto i = 0; i < s_iTestCoordsLength; i++) { - auto Coord = s_rgTestCoords[i]; + const auto Coord = s_rgTestCoords[i]; - fExpectedKeyHandled = (Coord.x <= MaxCoord && Coord.y <= MaxCoord); - Log::Comment(NoThrowString().Format(L"fHandled, x, y = (%d, %d, %d)", fExpectedKeyHandled, Coord.x, Coord.y)); - s_pwszInputExpected = BuildDefaultTestOutput(s_rgDefaultTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + TerminalInput::OutputType expected; + if (Coord.x <= MaxCoord && Coord.y <= MaxCoord) + { + expected = BuildDefaultTestOutput(s_rgDefaultTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + } // validate translation - VERIFY_ARE_EQUAL(fExpectedKeyHandled, - mouseInput->HandleMouse(Coord, - uiButton, - sModifierKeystate, - sScrollDelta, - {}), + VERIFY_ARE_EQUAL(expected, + mouseInput.HandleMouse(Coord, + uiButton, + sModifierKeystate, + sScrollDelta, + {}), NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.x, Coord.y)); } - mouseInput->SetInputMode(TerminalInput::Mode::AnyEventMouseTracking, true); + mouseInput.SetInputMode(TerminalInput::Mode::AnyEventMouseTracking, true); for (auto i = 0; i < s_iTestCoordsLength; i++) { - auto Coord = s_rgTestCoords[i]; + const auto Coord = s_rgTestCoords[i]; - fExpectedKeyHandled = (Coord.x <= MaxCoord && Coord.y <= MaxCoord); - Log::Comment(NoThrowString().Format(L"fHandled, x, y = (%d, %d, %d)", fExpectedKeyHandled, Coord.x, Coord.y)); - s_pwszInputExpected = BuildDefaultTestOutput(s_rgDefaultTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + TerminalInput::OutputType expected; + if (Coord.x <= MaxCoord && Coord.y <= MaxCoord) + { + expected = BuildDefaultTestOutput(s_rgDefaultTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + } // validate translation - VERIFY_ARE_EQUAL(fExpectedKeyHandled, - mouseInput->HandleMouse(Coord, - uiButton, - sModifierKeystate, - sScrollDelta, - {}), + VERIFY_ARE_EQUAL(expected, + mouseInput.HandleMouse(Coord, + uiButton, + sModifierKeystate, + sScrollDelta, + {}), NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.x, Coord.y)); } } @@ -454,7 +414,7 @@ class MouseInputTest Log::Comment(L"Starting test..."); - auto mouseInput = std::make_unique(s_MouseInputTestCallback); + TerminalInput mouseInput; unsigned int uiModifierKeystate = 0; VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiModifierKeystate", uiModifierKeystate)); auto sModifierKeystate = (SHORT)uiModifierKeystate; @@ -463,64 +423,65 @@ class MouseInputTest unsigned int uiButton; VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiButton", uiButton)); - auto fExpectedKeyHandled = false; - s_pwszInputExpected = L"\x0"; - VERIFY_ARE_EQUAL(fExpectedKeyHandled, mouseInput->HandleMouse({ 0, 0 }, uiButton, sModifierKeystate, sScrollDelta, {})); - - mouseInput->SetInputMode(TerminalInput::Mode::SgrMouseEncoding, true); + VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), mouseInput.HandleMouse({ 0, 0 }, uiButton, sModifierKeystate, sScrollDelta, {})); - // SGR Mode should be able to handle any arbitrary coords. - // However, mouse moves are only handled in Any Event mode - fExpectedKeyHandled = uiButton != WM_MOUSEMOVE; + mouseInput.SetInputMode(TerminalInput::Mode::SgrMouseEncoding, true); - mouseInput->SetInputMode(TerminalInput::Mode::DefaultMouseTracking, true); + mouseInput.SetInputMode(TerminalInput::Mode::DefaultMouseTracking, true); for (auto i = 0; i < s_iTestCoordsLength; i++) { - auto Coord = s_rgTestCoords[i]; + const auto Coord = s_rgTestCoords[i]; - Log::Comment(NoThrowString().Format(L"fHandled, x, y = (%d, %d, %d)", fExpectedKeyHandled, Coord.x, Coord.y)); - s_pwszInputExpected = BuildSGRTestOutput(s_rgSgrTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + // SGR Mode should be able to handle any arbitrary coords. + // However, mouse moves are only handled in Any Event mode + TerminalInput::OutputType expected; + if (uiButton != WM_MOUSEMOVE) + { + expected = BuildSGRTestOutput(s_rgSgrTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + } // validate translation - VERIFY_ARE_EQUAL(fExpectedKeyHandled, - mouseInput->HandleMouse(Coord, uiButton, sModifierKeystate, sScrollDelta, {}), + VERIFY_ARE_EQUAL(expected, + mouseInput.HandleMouse(Coord, uiButton, sModifierKeystate, sScrollDelta, {}), NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.x, Coord.y)); } - mouseInput->SetInputMode(TerminalInput::Mode::ButtonEventMouseTracking, true); + mouseInput.SetInputMode(TerminalInput::Mode::ButtonEventMouseTracking, true); for (auto i = 0; i < s_iTestCoordsLength; i++) { - auto Coord = s_rgTestCoords[i]; + const auto Coord = s_rgTestCoords[i]; - Log::Comment(NoThrowString().Format(L"fHandled, x, y = (%d, %d, %d)", fExpectedKeyHandled, Coord.x, Coord.y)); - s_pwszInputExpected = BuildSGRTestOutput(s_rgSgrTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + // SGR Mode should be able to handle any arbitrary coords. + // However, mouse moves are only handled in Any Event mode + TerminalInput::OutputType expected; + if (uiButton != WM_MOUSEMOVE) + { + expected = BuildSGRTestOutput(s_rgSgrTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + } // validate translation - VERIFY_ARE_EQUAL(fExpectedKeyHandled, - mouseInput->HandleMouse(Coord, - uiButton, - sModifierKeystate, - sScrollDelta, - {}), + VERIFY_ARE_EQUAL(expected, + mouseInput.HandleMouse(Coord, + uiButton, + sModifierKeystate, + sScrollDelta, + {}), NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.x, Coord.y)); } - fExpectedKeyHandled = true; - mouseInput->SetInputMode(TerminalInput::Mode::AnyEventMouseTracking, true); + mouseInput.SetInputMode(TerminalInput::Mode::AnyEventMouseTracking, true); for (auto i = 0; i < s_iTestCoordsLength; i++) { - auto Coord = s_rgTestCoords[i]; - - Log::Comment(NoThrowString().Format(L"fHandled, x, y = (%d, %d, %d)", fExpectedKeyHandled, Coord.x, Coord.y)); - s_pwszInputExpected = BuildSGRTestOutput(s_rgSgrTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + const auto Coord = s_rgTestCoords[i]; + const auto expected = BuildSGRTestOutput(s_rgSgrTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); // validate translation - VERIFY_ARE_EQUAL(fExpectedKeyHandled, - mouseInput->HandleMouse(Coord, - uiButton, - sModifierKeystate, - sScrollDelta, - {}), + VERIFY_ARE_EQUAL(expected, + mouseInput.HandleMouse(Coord, + uiButton, + sModifierKeystate, + sScrollDelta, + {}), NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.x, Coord.y)); } } @@ -535,7 +496,7 @@ class MouseInputTest Log::Comment(L"Starting test..."); - auto mouseInput = std::make_unique(s_MouseInputTestCallback); + TerminalInput mouseInput; unsigned int uiModifierKeystate = 0; VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiModifierKeystate", uiModifierKeystate)); auto sModifierKeystate = (SHORT)uiModifierKeystate; @@ -545,69 +506,68 @@ class MouseInputTest VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"sScrollDelta", iScrollDelta)); auto sScrollDelta = (short)(iScrollDelta); - auto fExpectedKeyHandled = false; - s_pwszInputExpected = L"\x0"; - VERIFY_ARE_EQUAL(fExpectedKeyHandled, mouseInput->HandleMouse({ 0, 0 }, uiButton, sModifierKeystate, sScrollDelta, {})); + VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), mouseInput.HandleMouse({ 0, 0 }, uiButton, sModifierKeystate, sScrollDelta, {})); // Default Tracking, Default Encoding - mouseInput->SetInputMode(TerminalInput::Mode::DefaultMouseTracking, true); + mouseInput.SetInputMode(TerminalInput::Mode::DefaultMouseTracking, true); for (auto i = 0; i < s_iTestCoordsLength; i++) { - auto Coord = s_rgTestCoords[i]; - fExpectedKeyHandled = (Coord.x <= 94 && Coord.y <= 94); + const auto Coord = s_rgTestCoords[i]; - Log::Comment(NoThrowString().Format(L"fHandled, x, y = (%d, %d, %d)", fExpectedKeyHandled, Coord.x, Coord.y)); - s_pwszInputExpected = BuildDefaultTestOutput(s_rgDefaultTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + TerminalInput::OutputType expected; + if (Coord.x <= 94 && Coord.y <= 94) + { + expected = BuildDefaultTestOutput(s_rgDefaultTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + } // validate translation - VERIFY_ARE_EQUAL(fExpectedKeyHandled, - mouseInput->HandleMouse(Coord, - uiButton, - sModifierKeystate, - sScrollDelta, - {}), + VERIFY_ARE_EQUAL(expected, + mouseInput.HandleMouse(Coord, + uiButton, + sModifierKeystate, + sScrollDelta, + {}), NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.x, Coord.y)); } // Default Tracking, UTF8 Encoding - mouseInput->SetInputMode(TerminalInput::Mode::Utf8MouseEncoding, true); + mouseInput.SetInputMode(TerminalInput::Mode::Utf8MouseEncoding, true); auto MaxCoord = SHORT_MAX - 33; for (auto i = 0; i < s_iTestCoordsLength; i++) { - auto Coord = s_rgTestCoords[i]; - fExpectedKeyHandled = (Coord.x <= MaxCoord && Coord.y <= MaxCoord); + const auto Coord = s_rgTestCoords[i]; - Log::Comment(NoThrowString().Format(L"fHandled, x, y = (%d, %d, %d)", fExpectedKeyHandled, Coord.x, Coord.y)); - s_pwszInputExpected = BuildDefaultTestOutput(s_rgDefaultTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + TerminalInput::OutputType expected; + if (Coord.x <= MaxCoord && Coord.y <= MaxCoord) + { + expected = BuildDefaultTestOutput(s_rgDefaultTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + } // validate translation - VERIFY_ARE_EQUAL(fExpectedKeyHandled, - mouseInput->HandleMouse(Coord, - uiButton, - sModifierKeystate, - sScrollDelta, - {}), + VERIFY_ARE_EQUAL(expected, + mouseInput.HandleMouse(Coord, + uiButton, + sModifierKeystate, + sScrollDelta, + {}), NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.x, Coord.y)); } // Default Tracking, SGR Encoding - mouseInput->SetInputMode(TerminalInput::Mode::SgrMouseEncoding, true); - fExpectedKeyHandled = true; // SGR Mode should be able to handle any arbitrary coords. + mouseInput.SetInputMode(TerminalInput::Mode::SgrMouseEncoding, true); for (auto i = 0; i < s_iTestCoordsLength; i++) { - auto Coord = s_rgTestCoords[i]; - - Log::Comment(NoThrowString().Format(L"fHandled, x, y = (%d, %d, %d)", fExpectedKeyHandled, Coord.x, Coord.y)); - s_pwszInputExpected = BuildSGRTestOutput(s_rgSgrTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + const auto Coord = s_rgTestCoords[i]; + const auto expected = BuildSGRTestOutput(s_rgSgrTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); // validate translation - VERIFY_ARE_EQUAL(fExpectedKeyHandled, - mouseInput->HandleMouse(Coord, - uiButton, - sModifierKeystate, - sScrollDelta, - {}), + VERIFY_ARE_EQUAL(expected, + mouseInput.HandleMouse(Coord, + uiButton, + sModifierKeystate, + sScrollDelta, + {}), NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.x, Coord.y)); } } @@ -615,40 +575,36 @@ class MouseInputTest TEST_METHOD(AlternateScrollModeTests) { Log::Comment(L"Starting test..."); - auto mouseInput = std::make_unique(s_MouseInputTestCallback); + TerminalInput mouseInput; const short noModifierKeys = 0; Log::Comment(L"Enable alternate scroll mode in the alt screen buffer"); - mouseInput->UseAlternateScreenBuffer(); - mouseInput->SetInputMode(TerminalInput::Mode::AlternateScroll, true); + mouseInput.UseAlternateScreenBuffer(); + mouseInput.SetInputMode(TerminalInput::Mode::AlternateScroll, true); Log::Comment(L"Test mouse wheel scrolling up"); - s_pwszInputExpected = L"\x1B[A"; - VERIFY_IS_TRUE(mouseInput->HandleMouse({ 0, 0 }, WM_MOUSEWHEEL, noModifierKeys, WHEEL_DELTA, {})); + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1B[A"), mouseInput.HandleMouse({ 0, 0 }, WM_MOUSEWHEEL, noModifierKeys, WHEEL_DELTA, {})); Log::Comment(L"Test mouse wheel scrolling down"); - s_pwszInputExpected = L"\x1B[B"; - VERIFY_IS_TRUE(mouseInput->HandleMouse({ 0, 0 }, WM_MOUSEWHEEL, noModifierKeys, -WHEEL_DELTA, {})); + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1B[B"), mouseInput.HandleMouse({ 0, 0 }, WM_MOUSEWHEEL, noModifierKeys, -WHEEL_DELTA, {})); Log::Comment(L"Enable cursor keys mode"); - mouseInput->SetInputMode(TerminalInput::Mode::CursorKey, true); + mouseInput.SetInputMode(TerminalInput::Mode::CursorKey, true); Log::Comment(L"Test mouse wheel scrolling up"); - s_pwszInputExpected = L"\x1BOA"; - VERIFY_IS_TRUE(mouseInput->HandleMouse({ 0, 0 }, WM_MOUSEWHEEL, noModifierKeys, WHEEL_DELTA, {})); + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1BOA"), mouseInput.HandleMouse({ 0, 0 }, WM_MOUSEWHEEL, noModifierKeys, WHEEL_DELTA, {})); Log::Comment(L"Test mouse wheel scrolling down"); - s_pwszInputExpected = L"\x1BOB"; - VERIFY_IS_TRUE(mouseInput->HandleMouse({ 0, 0 }, WM_MOUSEWHEEL, noModifierKeys, -WHEEL_DELTA, {})); + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1BOB"), mouseInput.HandleMouse({ 0, 0 }, WM_MOUSEWHEEL, noModifierKeys, -WHEEL_DELTA, {})); Log::Comment(L"Confirm no effect when scroll mode is disabled"); - mouseInput->UseAlternateScreenBuffer(); - mouseInput->SetInputMode(TerminalInput::Mode::AlternateScroll, false); - VERIFY_IS_FALSE(mouseInput->HandleMouse({ 0, 0 }, WM_MOUSEWHEEL, noModifierKeys, WHEEL_DELTA, {})); + mouseInput.UseAlternateScreenBuffer(); + mouseInput.SetInputMode(TerminalInput::Mode::AlternateScroll, false); + VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), mouseInput.HandleMouse({ 0, 0 }, WM_MOUSEWHEEL, noModifierKeys, WHEEL_DELTA, {})); Log::Comment(L"Confirm no effect when using the main buffer"); - mouseInput->UseMainScreenBuffer(); - mouseInput->SetInputMode(TerminalInput::Mode::AlternateScroll, true); - VERIFY_IS_FALSE(mouseInput->HandleMouse({ 0, 0 }, WM_MOUSEWHEEL, noModifierKeys, WHEEL_DELTA, {})); + mouseInput.UseMainScreenBuffer(); + mouseInput.SetInputMode(TerminalInput::Mode::AlternateScroll, true); + VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), mouseInput.HandleMouse({ 0, 0 }, WM_MOUSEWHEEL, noModifierKeys, WHEEL_DELTA, {})); } }; diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp index 7bb154966bd..0707e0694a1 100644 --- a/src/terminal/adapter/ut_adapter/adapterTest.cpp +++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp @@ -139,7 +139,7 @@ class TestGetSet final : public ITerminalApi } } - void UseAlternateScreenBuffer() override + void UseAlternateScreenBuffer(const TextAttribute& /*attr*/) override { Log::Comment(L"UseAlternateScreenBuffer MOCK called..."); } @@ -400,7 +400,7 @@ class AdapterTest if (fSuccess) { _testGetSet = std::move(api); - _terminalInput = TerminalInput{ nullptr }; + _terminalInput = TerminalInput{}; auto& renderer = _testGetSet->_renderer; auto& renderSettings = renderer._renderSettings; auto adapter = std::make_unique(*_testGetSet, renderer, renderSettings, _terminalInput); @@ -1799,7 +1799,7 @@ class AdapterTest // and DECRQM would not then be applicable. BEGIN_TEST_METHOD_PROPERTIES() - TEST_METHOD_PROPERTY(L"Data:modeNumber", L"{1, 3, 5, 6, 7, 8, 12, 25, 40, 66, 67, 69, 1000, 1002, 1003, 1004, 1005, 1006, 1007, 1049, 2004, 9001}") + TEST_METHOD_PROPERTY(L"Data:modeNumber", L"{1, 3, 5, 6, 7, 8, 12, 25, 40, 66, 67, 69, 117, 1000, 1002, 1003, 1004, 1005, 1006, 1007, 1049, 2004, 9001}") END_TEST_METHOD_PROPERTIES() VTInt modeNumber; @@ -2897,7 +2897,7 @@ class AdapterTest } private: - TerminalInput _terminalInput{ nullptr }; + TerminalInput _terminalInput; std::unique_ptr _testGetSet; AdaptDispatch* _pDispatch; // non-ownership pointer std::unique_ptr _stateMachine; diff --git a/src/terminal/adapter/ut_adapter/inputTest.cpp b/src/terminal/adapter/ut_adapter/inputTest.cpp index b86d5d95a45..9977cb609cf 100644 --- a/src/terminal/adapter/ut_adapter/inputTest.cpp +++ b/src/terminal/adapter/ut_adapter/inputTest.cpp @@ -27,18 +27,11 @@ namespace Microsoft }; using namespace Microsoft::Console::VirtualTerminal; -// For magic reasons, this has to live outside the class. Something wonderful about TAEF macros makes it -// invisible to the linker when inside the class. -static std::wstring s_expectedInput; - class Microsoft::Console::VirtualTerminal::InputTest { public: TEST_CLASS(InputTest); - static void s_TerminalInputTestCallback(_In_ std::deque>& inEvents); - static void s_TerminalInputTestNullCallback(_In_ std::deque>& inEvents); - TEST_METHOD(TerminalInputTests); TEST_METHOD(TestFocusEvents); TEST_METHOD(TerminalInputModifierKeyTests); @@ -79,91 +72,17 @@ class Microsoft::Console::VirtualTerminal::InputTest } }; -void InputTest::s_TerminalInputTestCallback(_In_ std::deque>& inEvents) -{ - auto records = IInputEvent::ToInputRecords(inEvents); - - if (VERIFY_ARE_EQUAL(s_expectedInput.size(), records.size(), L"Verify expected and actual input array lengths matched.")) - { - Log::Comment(L"We are expecting always key events and always key down. All other properties should not be written by simulated keys."); - - INPUT_RECORD irExpected = { 0 }; - irExpected.EventType = KEY_EVENT; - irExpected.Event.KeyEvent.bKeyDown = TRUE; - irExpected.Event.KeyEvent.wRepeatCount = 1; - - Log::Comment(L"Verifying individual array members..."); - for (size_t i = 0; i < records.size(); i++) - { - irExpected.Event.KeyEvent.uChar.UnicodeChar = s_expectedInput[i]; - VERIFY_ARE_EQUAL(irExpected, records[i], NoThrowString().Format(L"%c, %c", s_expectedInput[i], records[i].Event.KeyEvent.uChar.UnicodeChar)); - } - } -} - -void InputTest::s_TerminalInputTestNullCallback(_In_ std::deque>& inEvents) -{ - auto records = IInputEvent::ToInputRecords(inEvents); - - if (records.size() == 1) - { - Log::Comment(L"We are expecting a null input event."); - - INPUT_RECORD irExpected = { 0 }; - irExpected.EventType = KEY_EVENT; - irExpected.Event.KeyEvent.bKeyDown = TRUE; - irExpected.Event.KeyEvent.wRepeatCount = 1; - irExpected.Event.KeyEvent.wVirtualKeyCode = LOBYTE(OneCoreSafeVkKeyScanW(0)); - irExpected.Event.KeyEvent.dwControlKeyState = LEFT_CTRL_PRESSED; - irExpected.Event.KeyEvent.wVirtualScanCode = 0; - irExpected.Event.KeyEvent.uChar.UnicodeChar = L'\x0'; - - VERIFY_ARE_EQUAL(irExpected, records[0]); - } - else if (records.size() == 2) - { - Log::Comment(L"We are expecting a null input event, preceded by an escape"); - - INPUT_RECORD irExpectedEscape = { 0 }; - irExpectedEscape.EventType = KEY_EVENT; - irExpectedEscape.Event.KeyEvent.bKeyDown = TRUE; - irExpectedEscape.Event.KeyEvent.wRepeatCount = 1; - irExpectedEscape.Event.KeyEvent.wVirtualKeyCode = 0; - irExpectedEscape.Event.KeyEvent.dwControlKeyState = 0; - irExpectedEscape.Event.KeyEvent.wVirtualScanCode = 0; - irExpectedEscape.Event.KeyEvent.uChar.UnicodeChar = L'\x1b'; - - INPUT_RECORD irExpected = { 0 }; - irExpected.EventType = KEY_EVENT; - irExpected.Event.KeyEvent.bKeyDown = TRUE; - irExpected.Event.KeyEvent.wRepeatCount = 1; - irExpected.Event.KeyEvent.wVirtualKeyCode = 0; - irExpected.Event.KeyEvent.dwControlKeyState = 0; - irExpected.Event.KeyEvent.wVirtualScanCode = 0; - irExpected.Event.KeyEvent.uChar.UnicodeChar = L'\x0'; - - VERIFY_ARE_EQUAL(irExpectedEscape, records[0]); - VERIFY_ARE_EQUAL(irExpected, records[1]); - } - else - { - VERIFY_FAIL(NoThrowString().Format(L"Expected either one or two inputs, got %zu", records.size())); - } -} - void InputTest::TerminalInputTests() { Log::Comment(L"Starting test..."); - const auto pInput = new TerminalInput(s_TerminalInputTestCallback); + TerminalInput input; Log::Comment(L"Sending every possible VKEY at the input stream for interception during key DOWN."); for (BYTE vkey = 0; vkey < BYTE_MAX; vkey++) { Log::Comment(NoThrowString().Format(L"Testing Key 0x%x", vkey)); - auto fExpectedKeyHandled = true; - INPUT_RECORD irTest = { 0 }; irTest.EventType = KEY_EVENT; irTest.Event.KeyEvent.wRepeatCount = 1; @@ -171,103 +90,101 @@ void InputTest::TerminalInputTests() irTest.Event.KeyEvent.bKeyDown = TRUE; irTest.Event.KeyEvent.uChar.UnicodeChar = LOWORD(OneCoreSafeMapVirtualKeyW(vkey, MAPVK_VK_TO_CHAR)); - // Set up expected result + TerminalInput::OutputType expected; switch (vkey) { case VK_TAB: - s_expectedInput = L"\x09"; + expected = TerminalInput::MakeOutput(L"\x09"); break; case VK_BACK: - s_expectedInput = L"\x7f"; + expected = TerminalInput::MakeOutput(L"\x7f"); break; case VK_ESCAPE: - s_expectedInput = L"\x1b"; + expected = TerminalInput::MakeOutput(L"\x1b"); break; case VK_PAUSE: - s_expectedInput = L"\x1a"; + expected = TerminalInput::MakeOutput(L"\x1a"); break; case VK_UP: - s_expectedInput = L"\x1b[A"; + expected = TerminalInput::MakeOutput(L"\x1b[A"); break; case VK_DOWN: - s_expectedInput = L"\x1b[B"; + expected = TerminalInput::MakeOutput(L"\x1b[B"); break; case VK_RIGHT: - s_expectedInput = L"\x1b[C"; + expected = TerminalInput::MakeOutput(L"\x1b[C"); break; case VK_LEFT: - s_expectedInput = L"\x1b[D"; + expected = TerminalInput::MakeOutput(L"\x1b[D"); break; case VK_HOME: - s_expectedInput = L"\x1b[H"; + expected = TerminalInput::MakeOutput(L"\x1b[H"); break; case VK_INSERT: - s_expectedInput = L"\x1b[2~"; + expected = TerminalInput::MakeOutput(L"\x1b[2~"); break; case VK_DELETE: - s_expectedInput = L"\x1b[3~"; + expected = TerminalInput::MakeOutput(L"\x1b[3~"); break; case VK_END: - s_expectedInput = L"\x1b[F"; + expected = TerminalInput::MakeOutput(L"\x1b[F"); break; case VK_PRIOR: - s_expectedInput = L"\x1b[5~"; + expected = TerminalInput::MakeOutput(L"\x1b[5~"); break; case VK_NEXT: - s_expectedInput = L"\x1b[6~"; + expected = TerminalInput::MakeOutput(L"\x1b[6~"); break; case VK_F1: - s_expectedInput = L"\x1bOP"; + expected = TerminalInput::MakeOutput(L"\x1bOP"); break; case VK_F2: - s_expectedInput = L"\x1bOQ"; + expected = TerminalInput::MakeOutput(L"\x1bOQ"); break; case VK_F3: - s_expectedInput = L"\x1bOR"; + expected = TerminalInput::MakeOutput(L"\x1bOR"); break; case VK_F4: - s_expectedInput = L"\x1bOS"; + expected = TerminalInput::MakeOutput(L"\x1bOS"); break; case VK_F5: - s_expectedInput = L"\x1b[15~"; + expected = TerminalInput::MakeOutput(L"\x1b[15~"); break; case VK_F6: - s_expectedInput = L"\x1b[17~"; + expected = TerminalInput::MakeOutput(L"\x1b[17~"); break; case VK_F7: - s_expectedInput = L"\x1b[18~"; + expected = TerminalInput::MakeOutput(L"\x1b[18~"); break; case VK_F8: - s_expectedInput = L"\x1b[19~"; + expected = TerminalInput::MakeOutput(L"\x1b[19~"); break; case VK_F9: - s_expectedInput = L"\x1b[20~"; + expected = TerminalInput::MakeOutput(L"\x1b[20~"); break; case VK_F10: - s_expectedInput = L"\x1b[21~"; + expected = TerminalInput::MakeOutput(L"\x1b[21~"); break; case VK_F11: - s_expectedInput = L"\x1b[23~"; + expected = TerminalInput::MakeOutput(L"\x1b[23~"); break; case VK_F12: - s_expectedInput = L"\x1b[24~"; + expected = TerminalInput::MakeOutput(L"\x1b[24~"); break; case VK_CANCEL: - s_expectedInput = L"\x3"; + expected = TerminalInput::MakeOutput(L"\x3"); break; default: - fExpectedKeyHandled = false; + if (irTest.Event.KeyEvent.uChar.UnicodeChar != 0) + { + expected = TerminalInput::MakeOutput({ &irTest.Event.KeyEvent.uChar.UnicodeChar, 1 }); + } break; } - if (!fExpectedKeyHandled && irTest.Event.KeyEvent.uChar.UnicodeChar != 0) - { - s_expectedInput.clear(); - s_expectedInput.push_back(irTest.Event.KeyEvent.uChar.UnicodeChar); - fExpectedKeyHandled = true; - } + auto inputEvent = IInputEvent::Create(irTest); // Send key into object (will trigger callback and verification) - VERIFY_ARE_EQUAL(fExpectedKeyHandled, pInput->HandleKey(inputEvent.get()), L"Verify key was handled if it should have been."); + VERIFY_ARE_EQUAL(expected, input.HandleKey(inputEvent.get()), L"Verify key was handled if it should have been."); } Log::Comment(L"Sending every possible VKEY at the input stream for interception during key UP."); @@ -283,7 +200,7 @@ void InputTest::TerminalInputTests() auto inputEvent = IInputEvent::Create(irTest); // Send key into object (will trigger callback and verification) - VERIFY_ARE_EQUAL(false, pInput->HandleKey(inputEvent.get()), L"Verify key was NOT handled."); + VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), input.HandleKey(inputEvent.get()), L"Verify key was NOT handled."); } Log::Comment(L"Verify other types of events are not handled/intercepted."); @@ -293,17 +210,17 @@ void InputTest::TerminalInputTests() Log::Comment(L"Testing MOUSE_EVENT"); irUnhandled.EventType = MOUSE_EVENT; auto inputEvent = IInputEvent::Create(irUnhandled); - VERIFY_ARE_EQUAL(false, pInput->HandleKey(inputEvent.get()), L"Verify MOUSE_EVENT was NOT handled."); + VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), input.HandleKey(inputEvent.get()), L"Verify MOUSE_EVENT was NOT handled."); Log::Comment(L"Testing WINDOW_BUFFER_SIZE_EVENT"); irUnhandled.EventType = WINDOW_BUFFER_SIZE_EVENT; inputEvent = IInputEvent::Create(irUnhandled); - VERIFY_ARE_EQUAL(false, pInput->HandleKey(inputEvent.get()), L"Verify WINDOW_BUFFER_SIZE_EVENT was NOT handled."); + VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), input.HandleKey(inputEvent.get()), L"Verify WINDOW_BUFFER_SIZE_EVENT was NOT handled."); Log::Comment(L"Testing MENU_EVENT"); irUnhandled.EventType = MENU_EVENT; inputEvent = IInputEvent::Create(irUnhandled); - VERIFY_ARE_EQUAL(false, pInput->HandleKey(inputEvent.get()), L"Verify MENU_EVENT was NOT handled."); + VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), input.HandleKey(inputEvent.get()), L"Verify MENU_EVENT was NOT handled."); // Testing FOCUS_EVENTs is handled by TestFocusEvents } @@ -313,7 +230,7 @@ void InputTest::TestFocusEvents() // GH#12900, #13238 // Focus events that come in from the API should never be translated to VT sequences. // We're relying on the fact that the INPUT_RECORD version of the ctor is only called by the API - const auto pInput = new TerminalInput(s_TerminalInputTestCallback); + TerminalInput input; INPUT_RECORD irTest = { 0 }; irTest.EventType = FOCUS_EVENT; @@ -321,46 +238,34 @@ void InputTest::TestFocusEvents() { irTest.Event.FocusEvent.bSetFocus = false; auto inputEvent = IInputEvent::Create(irTest); - VERIFY_ARE_EQUAL(false, pInput->HandleKey(inputEvent.get()), L"Verify FOCUS_EVENT from API was NOT handled."); + VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), input.HandleKey(inputEvent.get()), L"Verify FOCUS_EVENT from API was NOT handled."); } { irTest.Event.FocusEvent.bSetFocus = true; auto inputEvent = IInputEvent::Create(irTest); - VERIFY_ARE_EQUAL(false, pInput->HandleKey(inputEvent.get()), L"Verify FOCUS_EVENT from API was NOT handled."); - } - { - auto inputEvent = std::make_unique(false); - VERIFY_ARE_EQUAL(false, pInput->HandleKey(inputEvent.get()), L"Verify FocusEvent from any other source was NOT handled."); - } - { - auto inputEvent = std::make_unique(true); - VERIFY_ARE_EQUAL(false, pInput->HandleKey(inputEvent.get()), L"Verify FocusEvent from any other source was NOT handled."); + VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), input.HandleKey(inputEvent.get()), L"Verify FOCUS_EVENT from API was NOT handled."); } + VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), input.HandleFocus(false), L"Verify FocusEvent from any other source was NOT handled."); + VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), input.HandleFocus(true), L"Verify FocusEvent from any other source was NOT handled."); + Log::Comment(L"Enable focus event handling"); - pInput->SetInputMode(TerminalInput::Mode::FocusEvent, true); + input.SetInputMode(TerminalInput::Mode::FocusEvent, true); { irTest.Event.FocusEvent.bSetFocus = false; auto inputEvent = IInputEvent::Create(irTest); - VERIFY_ARE_EQUAL(false, pInput->HandleKey(inputEvent.get()), L"Verify FOCUS_EVENT from API was NOT handled."); + VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), input.HandleKey(inputEvent.get()), L"Verify FOCUS_EVENT from API was NOT handled."); } { irTest.Event.FocusEvent.bSetFocus = true; auto inputEvent = IInputEvent::Create(irTest); - VERIFY_ARE_EQUAL(false, pInput->HandleKey(inputEvent.get()), L"Verify FOCUS_EVENT from API was NOT handled."); - } - { - s_expectedInput = L"\x1b[O"; - auto inputEvent = std::make_unique(false); - VERIFY_ARE_EQUAL(true, pInput->HandleKey(inputEvent.get()), L"Verify FocusEvent from any other source was handled."); - } - { - s_expectedInput = L"\x1b[I"; - auto inputEvent = std::make_unique(true); - VERIFY_ARE_EQUAL(true, pInput->HandleKey(inputEvent.get()), L"Verify FocusEvent from any other source was handled."); + VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), input.HandleKey(inputEvent.get()), L"Verify FOCUS_EVENT from API was NOT handled."); } + + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[O"), input.HandleFocus(false), L"Verify FocusEvent from any other source was handled."); + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b[I"), input.HandleFocus(true), L"Verify FocusEvent from any other source was handled."); } void InputTest::TerminalInputModifierKeyTests() @@ -379,7 +284,7 @@ void InputTest::TerminalInputModifierKeyTests() unsigned int uiKeystate; VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiModifierKeystate", uiKeystate)); - const auto pInput = new TerminalInput(s_TerminalInputTestCallback); + TerminalInput input; const auto slashVkey = LOBYTE(OneCoreSafeVkKeyScanW(L'/')); const auto nullVkey = LOBYTE(OneCoreSafeVkKeyScanW(0)); @@ -413,7 +318,7 @@ void InputTest::TerminalInputModifierKeyTests() } } - // Set up expected result + TerminalInput::OutputType expected; switch (vkey) { case VK_BACK: @@ -421,98 +326,98 @@ void InputTest::TerminalInputModifierKeyTests() case VK_OEM_2: // VK_OEM_2 is typically the '/?' key continue; - // s_expectedInput = L"\x7f"; + // expected = TerminalInput::MakeOutput(L"\x7f"); break; case VK_PAUSE: - s_expectedInput = L"\x1a"; + expected = TerminalInput::MakeOutput(L"\x1a"); break; case VK_UP: fModifySequence = true; - s_expectedInput = L"\x1b[1;mA"; + expected = TerminalInput::MakeOutput(L"\x1b[1;mA"); break; case VK_DOWN: fModifySequence = true; - s_expectedInput = L"\x1b[1;mB"; + expected = TerminalInput::MakeOutput(L"\x1b[1;mB"); break; case VK_RIGHT: fModifySequence = true; - s_expectedInput = L"\x1b[1;mC"; + expected = TerminalInput::MakeOutput(L"\x1b[1;mC"); break; case VK_LEFT: fModifySequence = true; - s_expectedInput = L"\x1b[1;mD"; + expected = TerminalInput::MakeOutput(L"\x1b[1;mD"); break; case VK_HOME: fModifySequence = true; - s_expectedInput = L"\x1b[1;mH"; + expected = TerminalInput::MakeOutput(L"\x1b[1;mH"); break; case VK_INSERT: fModifySequence = true; - s_expectedInput = L"\x1b[2;m~"; + expected = TerminalInput::MakeOutput(L"\x1b[2;m~"); break; case VK_DELETE: fModifySequence = true; - s_expectedInput = L"\x1b[3;m~"; + expected = TerminalInput::MakeOutput(L"\x1b[3;m~"); break; case VK_END: fModifySequence = true; - s_expectedInput = L"\x1b[1;mF"; + expected = TerminalInput::MakeOutput(L"\x1b[1;mF"); break; case VK_PRIOR: fModifySequence = true; - s_expectedInput = L"\x1b[5;m~"; + expected = TerminalInput::MakeOutput(L"\x1b[5;m~"); break; case VK_NEXT: fModifySequence = true; - s_expectedInput = L"\x1b[6;m~"; + expected = TerminalInput::MakeOutput(L"\x1b[6;m~"); break; case VK_F1: fModifySequence = true; - s_expectedInput = L"\x1b[1;mP"; + expected = TerminalInput::MakeOutput(L"\x1b[1;mP"); break; case VK_F2: fModifySequence = true; - s_expectedInput = L"\x1b[1;mQ"; + expected = TerminalInput::MakeOutput(L"\x1b[1;mQ"); break; case VK_F3: fModifySequence = true; - s_expectedInput = L"\x1b[1;mR"; + expected = TerminalInput::MakeOutput(L"\x1b[1;mR"); break; case VK_F4: fModifySequence = true; - s_expectedInput = L"\x1b[1;mS"; + expected = TerminalInput::MakeOutput(L"\x1b[1;mS"); break; case VK_F5: fModifySequence = true; - s_expectedInput = L"\x1b[15;m~"; + expected = TerminalInput::MakeOutput(L"\x1b[15;m~"); break; case VK_F6: fModifySequence = true; - s_expectedInput = L"\x1b[17;m~"; + expected = TerminalInput::MakeOutput(L"\x1b[17;m~"); break; case VK_F7: fModifySequence = true; - s_expectedInput = L"\x1b[18;m~"; + expected = TerminalInput::MakeOutput(L"\x1b[18;m~"); break; case VK_F8: fModifySequence = true; - s_expectedInput = L"\x1b[19;m~"; + expected = TerminalInput::MakeOutput(L"\x1b[19;m~"); break; case VK_F9: fModifySequence = true; - s_expectedInput = L"\x1b[20;m~"; + expected = TerminalInput::MakeOutput(L"\x1b[20;m~"); break; case VK_F10: fModifySequence = true; - s_expectedInput = L"\x1b[21;m~"; + expected = TerminalInput::MakeOutput(L"\x1b[21;m~"); break; case VK_F11: fModifySequence = true; - s_expectedInput = L"\x1b[23;m~"; + expected = TerminalInput::MakeOutput(L"\x1b[23;m~"); break; case VK_F12: fModifySequence = true; - s_expectedInput = L"\x1b[24;m~"; + expected = TerminalInput::MakeOutput(L"\x1b[24;m~"); break; case VK_TAB: if (AltPressed(uiKeystate)) @@ -522,11 +427,11 @@ void InputTest::TerminalInputModifierKeyTests() } else if (ShiftPressed(uiKeystate)) { - s_expectedInput = L"\x1b[Z"; + expected = TerminalInput::MakeOutput(L"\x1b[Z"); } else if (ControlPressed(uiKeystate)) { - s_expectedInput = L"\t"; + expected = TerminalInput::MakeOutput(L"\t"); } break; default: @@ -537,18 +442,16 @@ void InputTest::TerminalInputModifierKeyTests() // significant ones to be zeroed out (when using ASCII). if (AltPressed(uiKeystate) && ControlPressed(uiKeystate) && ch > 0x40 && ch <= 0x5A) { - s_expectedInput.clear(); - s_expectedInput.push_back(L'\x1b'); - s_expectedInput.push_back(ch & 0b11111); + const wchar_t buffer[2]{ L'\x1b', gsl::narrow_cast(ch & 0b11111) }; + expected = TerminalInput::MakeOutput({ &buffer[0], 2 }); break; } // Alt+Key generates [0x1b, key] into the stream if (AltPressed(uiKeystate) && !ControlPressed(uiKeystate) && ch != 0) { - s_expectedInput.clear(); - s_expectedInput.push_back(L'\x1b'); - s_expectedInput.push_back(ch); + const wchar_t buffer[2]{ L'\x1b', ch }; + expected = TerminalInput::MakeOutput({ &buffer[0], 2 }); break; } @@ -562,8 +465,7 @@ void InputTest::TerminalInputModifierKeyTests() if (ch != 0) { - s_expectedInput.clear(); - s_expectedInput.push_back(irTest.Event.KeyEvent.uChar.UnicodeChar); + expected = TerminalInput::MakeOutput({ &irTest.Event.KeyEvent.uChar.UnicodeChar, 1 }); break; } @@ -571,29 +473,28 @@ void InputTest::TerminalInputModifierKeyTests() break; } - if (fModifySequence && s_expectedInput.size() > 1) + if (fModifySequence) { auto fShift = !!(uiKeystate & SHIFT_PRESSED); auto fAlt = (uiKeystate & LEFT_ALT_PRESSED) || (uiKeystate & RIGHT_ALT_PRESSED); auto fCtrl = (uiKeystate & LEFT_CTRL_PRESSED) || (uiKeystate & RIGHT_CTRL_PRESSED); - s_expectedInput[s_expectedInput.size() - 2] = L'1' + (fShift ? 1 : 0) + (fAlt ? 2 : 0) + (fCtrl ? 4 : 0); + auto& str = expected.value(); + str[str.size() - 2] = L'1' + (fShift ? 1 : 0) + (fAlt ? 2 : 0) + (fCtrl ? 4 : 0); } - Log::Comment(NoThrowString().Format(L"Expected = \"%s\"", s_expectedInput.c_str())); - auto inputEvent = IInputEvent::Create(irTest); // Send key into object (will trigger callback and verification) - VERIFY_ARE_EQUAL(fExpectedKeyHandled, pInput->HandleKey(inputEvent.get()), L"Verify key was handled if it should have been."); + VERIFY_ARE_EQUAL(expected, input.HandleKey(inputEvent.get()), L"Verify key was handled if it should have been."); } } void InputTest::TerminalInputNullKeyTests() { - Log::Comment(L"Starting test..."); + using namespace std::string_view_literals; unsigned int uiKeystate = LEFT_CTRL_PRESSED; - const auto pInput = new TerminalInput(s_TerminalInputTestNullCallback); + TerminalInput input; Log::Comment(L"Sending every possible VKEY at the input stream for interception during key DOWN."); @@ -609,29 +510,29 @@ void InputTest::TerminalInputNullKeyTests() // Send key into object (will trigger callback and verification) auto inputEvent = IInputEvent::Create(irTest); - VERIFY_ARE_EQUAL(true, pInput->HandleKey(inputEvent.get()), L"Verify key was handled if it should have been."); + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\0"sv), input.HandleKey(inputEvent.get()), L"Verify key was handled if it should have been."); vkey = VK_SPACE; Log::Comment(NoThrowString().Format(L"Testing key, state =0x%x, 0x%x", vkey, uiKeystate)); irTest.Event.KeyEvent.wVirtualKeyCode = vkey; irTest.Event.KeyEvent.uChar.UnicodeChar = vkey; inputEvent = IInputEvent::Create(irTest); - VERIFY_ARE_EQUAL(true, pInput->HandleKey(inputEvent.get()), L"Verify key was handled if it should have been."); + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\0"sv), input.HandleKey(inputEvent.get()), L"Verify key was handled if it should have been."); uiKeystate = LEFT_CTRL_PRESSED | LEFT_ALT_PRESSED; Log::Comment(NoThrowString().Format(L"Testing key, state =0x%x, 0x%x", vkey, uiKeystate)); irTest.Event.KeyEvent.dwControlKeyState = uiKeystate; inputEvent = IInputEvent::Create(irTest); - VERIFY_ARE_EQUAL(true, pInput->HandleKey(inputEvent.get()), L"Verify key was handled if it should have been."); + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b\0"sv), input.HandleKey(inputEvent.get()), L"Verify key was handled if it should have been."); uiKeystate = RIGHT_CTRL_PRESSED | LEFT_ALT_PRESSED; Log::Comment(NoThrowString().Format(L"Testing key, state =0x%x, 0x%x", vkey, uiKeystate)); irTest.Event.KeyEvent.dwControlKeyState = uiKeystate; inputEvent = IInputEvent::Create(irTest); - VERIFY_ARE_EQUAL(true, pInput->HandleKey(inputEvent.get()), L"Verify key was handled if it should have been."); + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"\x1b\0"sv), input.HandleKey(inputEvent.get()), L"Verify key was handled if it should have been."); } -void TestKey(TerminalInput* const pInput, const unsigned int uiKeystate, const BYTE vkey, const wchar_t wch = 0) +static void TestKey(const TerminalInput::OutputType& expected, TerminalInput& input, const unsigned int uiKeystate, const BYTE vkey, const wchar_t wch = 0) { Log::Comment(NoThrowString().Format(L"Testing key, state =0x%x, 0x%x", vkey, uiKeystate)); @@ -645,265 +546,183 @@ void TestKey(TerminalInput* const pInput, const unsigned int uiKeystate, const B // Send key into object (will trigger callback and verification) auto inputEvent = IInputEvent::Create(irTest); - VERIFY_ARE_EQUAL(true, pInput->HandleKey(inputEvent.get()), L"Verify key was handled if it should have been."); + VERIFY_ARE_EQUAL(expected, input.HandleKey(inputEvent.get()), L"Verify key was handled if it should have been."); } void InputTest::DifferentModifiersTest() { Log::Comment(L"Starting test..."); - const auto pInput = new TerminalInput(s_TerminalInputTestCallback); + TerminalInput input; Log::Comment(L"Sending a bunch of keystrokes that are a little weird."); unsigned int uiKeystate = 0; BYTE vkey = VK_BACK; - s_expectedInput = L"\x7f"; - TestKey(pInput, uiKeystate, vkey); + TestKey(TerminalInput::MakeOutput(L"\x7f"), input, uiKeystate, vkey); uiKeystate = LEFT_CTRL_PRESSED; vkey = VK_BACK; - s_expectedInput = L"\x8"; - TestKey(pInput, uiKeystate, vkey, L'\x8'); + TestKey(TerminalInput::MakeOutput(L"\x8"), input, uiKeystate, vkey, L'\x8'); uiKeystate = RIGHT_CTRL_PRESSED; - TestKey(pInput, uiKeystate, vkey, L'\x8'); + TestKey(TerminalInput::MakeOutput(L"\x8"), input, uiKeystate, vkey, L'\x8'); uiKeystate = LEFT_ALT_PRESSED; vkey = VK_BACK; - s_expectedInput = L"\x1b\x7f"; - TestKey(pInput, uiKeystate, vkey, L'\x8'); + TestKey(TerminalInput::MakeOutput(L"\x1b\x7f"), input, uiKeystate, vkey, L'\x8'); uiKeystate = RIGHT_ALT_PRESSED; - TestKey(pInput, uiKeystate, vkey, L'\x8'); + TestKey(TerminalInput::MakeOutput(L"\x1b\x7f"), input, uiKeystate, vkey, L'\x8'); uiKeystate = LEFT_CTRL_PRESSED; vkey = VK_DELETE; - s_expectedInput = L"\x1b[3;5~"; - TestKey(pInput, uiKeystate, vkey); + TestKey(TerminalInput::MakeOutput(L"\x1b[3;5~"), input, uiKeystate, vkey); uiKeystate = RIGHT_CTRL_PRESSED; - TestKey(pInput, uiKeystate, vkey); + TestKey(TerminalInput::MakeOutput(L"\x1b[3;5~"), input, uiKeystate, vkey); uiKeystate = LEFT_ALT_PRESSED; vkey = VK_DELETE; - s_expectedInput = L"\x1b[3;3~"; - TestKey(pInput, uiKeystate, vkey); + TestKey(TerminalInput::MakeOutput(L"\x1b[3;3~"), input, uiKeystate, vkey); uiKeystate = RIGHT_ALT_PRESSED; - TestKey(pInput, uiKeystate, vkey); + TestKey(TerminalInput::MakeOutput(L"\x1b[3;3~"), input, uiKeystate, vkey); uiKeystate = LEFT_CTRL_PRESSED; vkey = VK_TAB; - s_expectedInput = L"\t"; - TestKey(pInput, uiKeystate, vkey); + TestKey(TerminalInput::MakeOutput(L"\t"), input, uiKeystate, vkey); uiKeystate = RIGHT_CTRL_PRESSED; - TestKey(pInput, uiKeystate, vkey); + TestKey(TerminalInput::MakeOutput(L"\t"), input, uiKeystate, vkey); uiKeystate = SHIFT_PRESSED; vkey = VK_TAB; - s_expectedInput = L"\x1b[Z"; - TestKey(pInput, uiKeystate, vkey); + TestKey(TerminalInput::MakeOutput(L"\x1b[Z"), input, uiKeystate, vkey); // C-/ -> C-_ -> 0x1f uiKeystate = LEFT_CTRL_PRESSED; vkey = LOBYTE(OneCoreSafeVkKeyScanW(L'/')); - s_expectedInput = L"\x1f"; - TestKey(pInput, uiKeystate, vkey, L'/'); + TestKey(TerminalInput::MakeOutput(L"\x1f"), input, uiKeystate, vkey, L'/'); uiKeystate = RIGHT_CTRL_PRESSED; - TestKey(pInput, uiKeystate, vkey, L'/'); + TestKey(TerminalInput::MakeOutput(L"\x1f"), input, uiKeystate, vkey, L'/'); // M-/ -> ESC / uiKeystate = LEFT_ALT_PRESSED; vkey = LOBYTE(OneCoreSafeVkKeyScanW(L'/')); - s_expectedInput = L"\x1b/"; - TestKey(pInput, uiKeystate, vkey, L'/'); + TestKey(TerminalInput::MakeOutput(L"\x1b/"), input, uiKeystate, vkey, L'/'); uiKeystate = RIGHT_ALT_PRESSED; - TestKey(pInput, uiKeystate, vkey, L'/'); + TestKey(TerminalInput::MakeOutput(L"\x1b/"), input, uiKeystate, vkey, L'/'); // See https://github.com/microsoft/terminal/pull/4947#issuecomment-600382856 // C-? -> DEL -> 0x7f Log::Comment(NoThrowString().Format(L"Checking C-?")); // Use SHIFT_PRESSED to force us into differentiating between '/' and '?' vkey = LOBYTE(OneCoreSafeVkKeyScanW(L'?')); - s_expectedInput = L"\x7f"; - TestKey(pInput, SHIFT_PRESSED | LEFT_CTRL_PRESSED, vkey, L'?'); - TestKey(pInput, SHIFT_PRESSED | RIGHT_CTRL_PRESSED, vkey, L'?'); + TestKey(TerminalInput::MakeOutput(L"\x7f"), input, SHIFT_PRESSED | LEFT_CTRL_PRESSED, vkey, L'?'); + TestKey(TerminalInput::MakeOutput(L"\x7f"), input, SHIFT_PRESSED | RIGHT_CTRL_PRESSED, vkey, L'?'); // C-M-/ -> 0x1b0x1f Log::Comment(NoThrowString().Format(L"Checking C-M-/")); uiKeystate = LEFT_CTRL_PRESSED | LEFT_ALT_PRESSED; vkey = LOBYTE(OneCoreSafeVkKeyScanW(L'/')); - s_expectedInput = L"\x1b\x1f"; - TestKey(pInput, LEFT_CTRL_PRESSED | LEFT_ALT_PRESSED, vkey, L'/'); - TestKey(pInput, RIGHT_CTRL_PRESSED | LEFT_ALT_PRESSED, vkey, L'/'); + TestKey(TerminalInput::MakeOutput(L"\x1b\x1f"), input, LEFT_CTRL_PRESSED | LEFT_ALT_PRESSED, vkey, L'/'); + TestKey(TerminalInput::MakeOutput(L"\x1b\x1f"), input, RIGHT_CTRL_PRESSED | LEFT_ALT_PRESSED, vkey, L'/'); // LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED is skipped because that's AltGr - TestKey(pInput, RIGHT_CTRL_PRESSED | RIGHT_ALT_PRESSED, vkey, L'/'); + TestKey(TerminalInput::MakeOutput(L"\x1b\x1f"), input, RIGHT_CTRL_PRESSED | RIGHT_ALT_PRESSED, vkey, L'/'); // C-M-? -> 0x1b0x7f Log::Comment(NoThrowString().Format(L"Checking C-M-?")); uiKeystate = LEFT_CTRL_PRESSED | LEFT_ALT_PRESSED; vkey = LOBYTE(OneCoreSafeVkKeyScanW(L'?')); - s_expectedInput = L"\x1b\x7f"; - TestKey(pInput, SHIFT_PRESSED | LEFT_CTRL_PRESSED | LEFT_ALT_PRESSED, vkey, L'?'); - TestKey(pInput, SHIFT_PRESSED | RIGHT_CTRL_PRESSED | LEFT_ALT_PRESSED, vkey, L'?'); + TestKey(TerminalInput::MakeOutput(L"\x1b\x7f"), input, SHIFT_PRESSED | LEFT_CTRL_PRESSED | LEFT_ALT_PRESSED, vkey, L'?'); + TestKey(TerminalInput::MakeOutput(L"\x1b\x7f"), input, SHIFT_PRESSED | RIGHT_CTRL_PRESSED | LEFT_ALT_PRESSED, vkey, L'?'); // LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED is skipped because that's AltGr - TestKey(pInput, SHIFT_PRESSED | RIGHT_CTRL_PRESSED | RIGHT_ALT_PRESSED, vkey, L'?'); + TestKey(TerminalInput::MakeOutput(L"\x1b\x7f"), input, SHIFT_PRESSED | RIGHT_CTRL_PRESSED | RIGHT_ALT_PRESSED, vkey, L'?'); } void InputTest::CtrlNumTest() { Log::Comment(L"Starting test..."); - const auto pInput = new TerminalInput(s_TerminalInputTestCallback); + TerminalInput input; Log::Comment(L"Sending the various Ctrl+Num keys."); unsigned int uiKeystate = LEFT_CTRL_PRESSED; BYTE vkey = static_cast('1'); - s_expectedInput = L"1"; - TestKey(pInput, uiKeystate, vkey); + TestKey(TerminalInput::MakeOutput(L"1"), input, uiKeystate, vkey); Log::Comment(NoThrowString().Format( L"Skipping Ctrl+2, since that's supposed to send NUL, and doesn't play " L"nicely with this test. Ctrl+2 is covered by other tests in this class.")); vkey = static_cast('3'); - s_expectedInput = L"\x1b"; - TestKey(pInput, uiKeystate, vkey); + TestKey(TerminalInput::MakeOutput(L"\x1b"), input, uiKeystate, vkey); vkey = static_cast('4'); - s_expectedInput = L"\x1c"; - TestKey(pInput, uiKeystate, vkey); + TestKey(TerminalInput::MakeOutput(L"\x1c"), input, uiKeystate, vkey); vkey = static_cast('5'); - s_expectedInput = L"\x1d"; - TestKey(pInput, uiKeystate, vkey); + TestKey(TerminalInput::MakeOutput(L"\x1d"), input, uiKeystate, vkey); vkey = static_cast('6'); - s_expectedInput = L"\x1e"; - TestKey(pInput, uiKeystate, vkey); + TestKey(TerminalInput::MakeOutput(L"\x1e"), input, uiKeystate, vkey); vkey = static_cast('7'); - s_expectedInput = L"\x1f"; - TestKey(pInput, uiKeystate, vkey); + TestKey(TerminalInput::MakeOutput(L"\x1f"), input, uiKeystate, vkey); vkey = static_cast('8'); - s_expectedInput = L"\x7f"; - TestKey(pInput, uiKeystate, vkey); + TestKey(TerminalInput::MakeOutput(L"\x7f"), input, uiKeystate, vkey); vkey = static_cast('9'); - s_expectedInput = L"9"; - TestKey(pInput, uiKeystate, vkey); + TestKey(TerminalInput::MakeOutput(L"9"), input, uiKeystate, vkey); } void InputTest::BackarrowKeyModeTest() { Log::Comment(L"Starting test..."); - const auto pInput = new TerminalInput(s_TerminalInputTestCallback); + TerminalInput input; const BYTE vkey = VK_BACK; Log::Comment(L"Sending backspace key combos with DECBKM enabled."); - pInput->SetInputMode(TerminalInput::Mode::BackarrowKey, true); - - s_expectedInput = L"\x8"; - TestKey(pInput, 0, vkey); - - s_expectedInput = L"\x8"; - TestKey(pInput, SHIFT_PRESSED, vkey); - - s_expectedInput = L"\x7f"; - TestKey(pInput, LEFT_CTRL_PRESSED, vkey); - - s_expectedInput = L"\x7f"; - TestKey(pInput, LEFT_CTRL_PRESSED | SHIFT_PRESSED, vkey); - - s_expectedInput = L"\x1b\x8"; - TestKey(pInput, LEFT_ALT_PRESSED, vkey); - - s_expectedInput = L"\x1b\x8"; - TestKey(pInput, LEFT_ALT_PRESSED | SHIFT_PRESSED, vkey); - - s_expectedInput = L"\x1b\x7f"; - TestKey(pInput, LEFT_ALT_PRESSED | LEFT_CTRL_PRESSED, vkey); - - s_expectedInput = L"\x1b\x7f"; - TestKey(pInput, LEFT_ALT_PRESSED | LEFT_CTRL_PRESSED | SHIFT_PRESSED, vkey); + input.SetInputMode(TerminalInput::Mode::BackarrowKey, true); + TestKey(TerminalInput::MakeOutput(L"\x8"), input, 0, vkey); + TestKey(TerminalInput::MakeOutput(L"\x8"), input, SHIFT_PRESSED, vkey); + TestKey(TerminalInput::MakeOutput(L"\x7f"), input, LEFT_CTRL_PRESSED, vkey); + TestKey(TerminalInput::MakeOutput(L"\x7f"), input, LEFT_CTRL_PRESSED | SHIFT_PRESSED, vkey); + TestKey(TerminalInput::MakeOutput(L"\x1b\x8"), input, LEFT_ALT_PRESSED, vkey); + TestKey(TerminalInput::MakeOutput(L"\x1b\x8"), input, LEFT_ALT_PRESSED | SHIFT_PRESSED, vkey); + TestKey(TerminalInput::MakeOutput(L"\x1b\x7f"), input, LEFT_ALT_PRESSED | LEFT_CTRL_PRESSED, vkey); + TestKey(TerminalInput::MakeOutput(L"\x1b\x7f"), input, LEFT_ALT_PRESSED | LEFT_CTRL_PRESSED | SHIFT_PRESSED, vkey); Log::Comment(L"Sending backspace key combos with DECBKM disabled."); - pInput->SetInputMode(TerminalInput::Mode::BackarrowKey, false); - - s_expectedInput = L"\x7f"; - TestKey(pInput, 0, vkey); - - s_expectedInput = L"\x7f"; - TestKey(pInput, SHIFT_PRESSED, vkey); - - s_expectedInput = L"\x8"; - TestKey(pInput, LEFT_CTRL_PRESSED, vkey); - - s_expectedInput = L"\x8"; - TestKey(pInput, LEFT_CTRL_PRESSED | SHIFT_PRESSED, vkey); - - s_expectedInput = L"\x1b\x7f"; - TestKey(pInput, LEFT_ALT_PRESSED, vkey); - - s_expectedInput = L"\x1b\x7f"; - TestKey(pInput, LEFT_ALT_PRESSED | SHIFT_PRESSED, vkey); - - s_expectedInput = L"\x1b\x8"; - TestKey(pInput, LEFT_ALT_PRESSED | LEFT_CTRL_PRESSED, vkey); - - s_expectedInput = L"\x1b\x8"; - TestKey(pInput, LEFT_ALT_PRESSED | LEFT_CTRL_PRESSED | SHIFT_PRESSED, vkey); + input.SetInputMode(TerminalInput::Mode::BackarrowKey, false); + TestKey(TerminalInput::MakeOutput(L"\x7f"), input, 0, vkey); + TestKey(TerminalInput::MakeOutput(L"\x7f"), input, SHIFT_PRESSED, vkey); + TestKey(TerminalInput::MakeOutput(L"\x8"), input, LEFT_CTRL_PRESSED, vkey); + TestKey(TerminalInput::MakeOutput(L"\x8"), input, LEFT_CTRL_PRESSED | SHIFT_PRESSED, vkey); + TestKey(TerminalInput::MakeOutput(L"\x1b\x7f"), input, LEFT_ALT_PRESSED, vkey); + TestKey(TerminalInput::MakeOutput(L"\x1b\x7f"), input, LEFT_ALT_PRESSED | SHIFT_PRESSED, vkey); + TestKey(TerminalInput::MakeOutput(L"\x1b\x8"), input, LEFT_ALT_PRESSED | LEFT_CTRL_PRESSED, vkey); + TestKey(TerminalInput::MakeOutput(L"\x1b\x8"), input, LEFT_ALT_PRESSED | LEFT_CTRL_PRESSED | SHIFT_PRESSED, vkey); } void InputTest::AutoRepeatModeTest() { - Log::Comment(L"Starting test..."); - - auto receivedChars = std::wstring{}; - const auto pInput = new TerminalInput([&](auto& inEvents) { - for (auto& record : IInputEvent::ToInputRecords(inEvents)) - { - receivedChars.push_back(record.Event.KeyEvent.uChar.UnicodeChar); - } - }); - - const auto repeatKey = [&](auto vKey, auto unicodeChar, int repeatCount) { - auto irTest = INPUT_RECORD{ 0 }; - irTest.EventType = KEY_EVENT; - irTest.Event.KeyEvent.wRepeatCount = 1; - irTest.Event.KeyEvent.wVirtualKeyCode = vKey; - irTest.Event.KeyEvent.uChar.UnicodeChar = unicodeChar; - irTest.Event.KeyEvent.bKeyDown = TRUE; - - // We're simulating a key being held down so that it repeats, by sending - // multiple KeyDown events followed by a single KeyUp event. - - for (auto i = 0; i < repeatCount; i++) - { - auto keyDownEvent = IInputEvent::Create(irTest); - VERIFY_IS_TRUE(pInput->HandleKey(keyDownEvent.get())); - } - - irTest.Event.KeyEvent.bKeyDown = FALSE; - auto keyUpEvent = IInputEvent::Create(irTest); - VERIFY_IS_FALSE(pInput->HandleKey(keyUpEvent.get())); - }; + const auto down = std::make_unique(true, 1ui16, 'A', 0ui16, 'A', 0ui16); + const auto up = std::make_unique(false, 1ui16, 'A', 0ui16, 'A', 0ui16); + TerminalInput input; Log::Comment(L"Sending repeating keypresses with DECARM disabled."); - pInput->SetInputMode(TerminalInput::Mode::AutoRepeat, false); - receivedChars.clear(); - repeatKey('A', L'a', 5); - repeatKey('B', L'b', 5); - repeatKey('C', L'c', 5); - VERIFY_ARE_EQUAL(L"abc", receivedChars); + input.SetInputMode(TerminalInput::Mode::AutoRepeat, false); + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"A"), input.HandleKey(down.get())); + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput({}), input.HandleKey(down.get())); + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput({}), input.HandleKey(down.get())); + VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), input.HandleKey(up.get())); Log::Comment(L"Sending repeating keypresses with DECARM enabled."); - pInput->SetInputMode(TerminalInput::Mode::AutoRepeat, true); - receivedChars.clear(); - repeatKey('A', L'a', 5); - repeatKey('B', L'b', 5); - repeatKey('C', L'c', 5); - VERIFY_ARE_EQUAL(L"aaaaabbbbbccccc", receivedChars); + input.SetInputMode(TerminalInput::Mode::AutoRepeat, true); + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"A"), input.HandleKey(down.get())); + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"A"), input.HandleKey(down.get())); + VERIFY_ARE_EQUAL(TerminalInput::MakeOutput(L"A"), input.HandleKey(down.get())); + VERIFY_ARE_EQUAL(TerminalInput::MakeUnhandled(), input.HandleKey(up.get())); } diff --git a/src/terminal/input/mouseInput.cpp b/src/terminal/input/mouseInput.cpp index e80d3df4c3a..1fe64047b80 100644 --- a/src/terminal/input/mouseInput.cpp +++ b/src/terminal/input/mouseInput.cpp @@ -2,10 +2,9 @@ // Licensed under the MIT license. #include "precomp.h" -#include #include "terminalInput.hpp" + #include "../types/inc/utils.hpp" -#include "../../interactivity/inc/VtApiRedirection.hpp" using namespace Microsoft::Console::VirtualTerminal; @@ -297,12 +296,9 @@ bool TerminalInput::IsTrackingMouseInput() const noexcept // - delta - the amount that the scroll wheel changed (should be 0 unless button is a WM_MOUSE*WHEEL) // - state - the state of the mouse buttons at this moment // Return value: -// - true if the event was handled and we should stop event propagation to the default window handler. -bool TerminalInput::HandleMouse(const til::point position, - const unsigned int button, - const short modifierKeyState, - const short delta, - const MouseButtonState state) +// - Returns an empty optional if we didn't handle the mouse event and the caller can opt to handle it in some other way. +// - Returns a string if we successfully translated it into a VT input sequence. +TerminalInput::OutputType TerminalInput::HandleMouse(const til::point position, const unsigned int button, const short modifierKeyState, const short delta, const MouseButtonState state) { if (Utils::Sign(delta) != Utils::Sign(_mouseInputState.accumulatedDelta)) { @@ -322,7 +318,14 @@ bool TerminalInput::HandleMouse(const til::point position, // on the wheel, accumulate delta until we hit the amount required to dispatch one // "line" worth of scroll. // Mark the event as "handled" if we would have otherwise emitted a scroll event. - return IsTrackingMouseInput() || ShouldSendAlternateScroll(button, delta); + OutputType out; + if (IsTrackingMouseInput() || ShouldSendAlternateScroll(button, delta)) + { + // Emplacing an empty string marks the return value as "we handled it" + // but since it's empty it will not produce any actual output. + out.emplace(); + } + return out; } // We're ready to send this event through, but first we need to clear the accumulated; @@ -330,90 +333,65 @@ bool TerminalInput::HandleMouse(const til::point position, _mouseInputState.accumulatedDelta = 0; } - auto success = false; if (ShouldSendAlternateScroll(button, delta)) { - success = _SendAlternateScroll(delta); + return _makeAlternateScrollOutput(delta); } - else + + if (IsTrackingMouseInput()) { - success = IsTrackingMouseInput(); - if (success) + // isHover is only true for WM_MOUSEMOVE events + const auto isHover = _isHoverMsg(button); + const auto isButton = _isButtonMsg(button); + + const auto sameCoord = (position.x == _mouseInputState.lastPos.x) && + (position.y == _mouseInputState.lastPos.y) && + (_mouseInputState.lastButton == button); + + // If we have a WM_MOUSEMOVE, we need to know if any of the mouse + // buttons are actually pressed. If they are, + // _GetPressedButton will return the first pressed mouse button. + // If it returns WM_LBUTTONUP, then we can assume that the mouse + // moved without a button being pressed. + const auto realButton = isHover ? s_GetPressedButton(state) : button; + + // In default mode, only button presses/releases are sent + // In ButtonEvent mode, changing coord hovers WITH A BUTTON PRESSED + // (WM_LBUTTONUP is our sentinel that no button was pressed) are also sent. + // In AnyEvent, all coord change hovers are sent + const auto physicalButtonPressed = realButton != WM_LBUTTONUP; + + if (isButton || + (isHover && _inputMode.test(Mode::ButtonEventMouseTracking) && ((!sameCoord) && (physicalButtonPressed))) || + (isHover && _inputMode.test(Mode::AnyEventMouseTracking) && !sameCoord)) { - // isHover is only true for WM_MOUSEMOVE events - const auto isHover = _isHoverMsg(button); - const auto isButton = _isButtonMsg(button); - - const auto sameCoord = (position.x == _mouseInputState.lastPos.x) && - (position.y == _mouseInputState.lastPos.y) && - (_mouseInputState.lastButton == button); - - // If we have a WM_MOUSEMOVE, we need to know if any of the mouse - // buttons are actually pressed. If they are, - // _GetPressedButton will return the first pressed mouse button. - // If it returns WM_LBUTTONUP, then we can assume that the mouse - // moved without a button being pressed. - const auto realButton = isHover ? s_GetPressedButton(state) : button; - - // In default mode, only button presses/releases are sent - // In ButtonEvent mode, changing coord hovers WITH A BUTTON PRESSED - // (WM_LBUTTONUP is our sentinel that no button was pressed) are also sent. - // In AnyEvent, all coord change hovers are sent - const auto physicalButtonPressed = realButton != WM_LBUTTONUP; - - success = (isButton && IsTrackingMouseInput()) || - (isHover && _inputMode.test(Mode::ButtonEventMouseTracking) && ((!sameCoord) && (physicalButtonPressed))) || - (isHover && _inputMode.test(Mode::AnyEventMouseTracking) && !sameCoord); - - if (success) + if (_inputMode.any(Mode::ButtonEventMouseTracking, Mode::AnyEventMouseTracking)) + { + _mouseInputState.lastPos.x = position.x; + _mouseInputState.lastPos.y = position.y; + _mouseInputState.lastButton = button; + } + + if (_inputMode.test(Mode::Utf8MouseEncoding)) + { + return _GenerateUtf8Sequence(position, realButton, isHover, modifierKeyState, delta); + } + else if (_inputMode.test(Mode::SgrMouseEncoding)) + { + // For SGR encoding, if no physical buttons were pressed, + // then we want to handle hovers with WM_MOUSEMOVE. + // However, if we're dragging (WM_MOUSEMOVE with a button pressed), + // then use that pressed button instead. + return _GenerateSGRSequence(position, physicalButtonPressed ? realButton : button, _isButtonDown(realButton), isHover, modifierKeyState, delta); + } + else { - std::wstring sequence; - if (_inputMode.test(Mode::Utf8MouseEncoding)) - { - sequence = _GenerateUtf8Sequence(position, - realButton, - isHover, - modifierKeyState, - delta); - } - else if (_inputMode.test(Mode::SgrMouseEncoding)) - { - // For SGR encoding, if no physical buttons were pressed, - // then we want to handle hovers with WM_MOUSEMOVE. - // However, if we're dragging (WM_MOUSEMOVE with a button pressed), - // then use that pressed button instead. - sequence = _GenerateSGRSequence(position, - physicalButtonPressed ? realButton : button, - _isButtonDown(realButton), // Use realButton here, to properly get the up/down state - isHover, - modifierKeyState, - delta); - } - else - { - sequence = _GenerateDefaultSequence(position, - realButton, - isHover, - modifierKeyState, - delta); - } - success = !sequence.empty(); - - if (success) - { - _SendInputSequence(sequence); - success = true; - } - if (_inputMode.any(Mode::ButtonEventMouseTracking, Mode::AnyEventMouseTracking)) - { - _mouseInputState.lastPos.x = position.x; - _mouseInputState.lastPos.y = position.y; - _mouseInputState.lastButton = button; - } + return _GenerateDefaultSequence(position, realButton, isHover, modifierKeyState, delta); } } } - return success; + + return {}; } // Routine Description: @@ -425,13 +403,7 @@ bool TerminalInput::HandleMouse(const til::point position, // - isHover - true if the sequence is generated in response to a mouse hover // - modifierKeyState - the modifier keys pressed with this button // - delta - the amount that the scroll wheel changed (should be 0 unless button is a WM_MOUSE*WHEEL) -// Return value: -// - The generated sequence. Will be empty if we couldn't generate. -std::wstring TerminalInput::_GenerateDefaultSequence(const til::point position, - const unsigned int button, - const bool isHover, - const short modifierKeyState, - const short delta) +TerminalInput::OutputType TerminalInput::_GenerateDefaultSequence(const til::point position, const unsigned int button, const bool isHover, const short modifierKeyState, const short delta) { // In the default, non-extended encoding scheme, coordinates above 94 shouldn't be supported, // because (95+32+1)=128, which is not an ASCII character. @@ -443,7 +415,7 @@ std::wstring TerminalInput::_GenerateDefaultSequence(const til::point position, const auto encodedX = _encodeDefaultCoordinate(vtCoords.x); const auto encodedY = _encodeDefaultCoordinate(vtCoords.y); - std::wstring format{ L"\x1b[Mbxy" }; + StringType format{ L"\x1b[Mbxy" }; til::at(format, 3) = gsl::narrow_cast(L' ' + _windowsButtonToXEncoding(button, isHover, modifierKeyState, delta)); til::at(format, 4) = gsl::narrow_cast(encodedX); til::at(format, 5) = gsl::narrow_cast(encodedY); @@ -464,11 +436,7 @@ std::wstring TerminalInput::_GenerateDefaultSequence(const til::point position, // - delta - the amount that the scroll wheel changed (should be 0 unless button is a WM_MOUSE*WHEEL) // Return value: // - The generated sequence. Will be empty if we couldn't generate. -std::wstring TerminalInput::_GenerateUtf8Sequence(const til::point position, - const unsigned int button, - const bool isHover, - const short modifierKeyState, - const short delta) +TerminalInput::OutputType TerminalInput::_GenerateUtf8Sequence(const til::point position, const unsigned int button, const bool isHover, const short modifierKeyState, const short delta) { // So we have some complications here. // The windows input stream is typically encoded as UTF16. @@ -489,7 +457,8 @@ std::wstring TerminalInput::_GenerateUtf8Sequence(const til::point position, const auto vtCoords = _winToVTCoord(position); const auto encodedX = _encodeDefaultCoordinate(vtCoords.x); const auto encodedY = _encodeDefaultCoordinate(vtCoords.y); - std::wstring format{ L"\x1b[Mbxy" }; + + StringType format{ L"\x1b[Mbxy" }; // The short cast is safe because we know s_WindowsButtonToXEncoding never returns more than xff til::at(format, 3) = gsl::narrow_cast(L' ' + _windowsButtonToXEncoding(button, isHover, modifierKeyState, delta)); til::at(format, 4) = gsl::narrow_cast(encodedX); @@ -512,23 +481,12 @@ std::wstring TerminalInput::_GenerateUtf8Sequence(const til::point position, // - delta - the amount that the scroll wheel changed (should be 0 unless button is a WM_MOUSE*WHEEL) // - ppwchSequence - On success, where to put the pointer to the generated sequence // - pcchLength - On success, where to put the length of the generated sequence -// Return value: -// - true if we were able to successfully generate a sequence. -// On success, caller is responsible for delete[]ing *ppwchSequence. -std::wstring TerminalInput::_GenerateSGRSequence(const til::point position, - const unsigned int button, - const bool isDown, - const bool isHover, - const short modifierKeyState, - const short delta) +TerminalInput::OutputType TerminalInput::_GenerateSGRSequence(const til::point position, const unsigned int button, const bool isDown, const bool isHover, const short modifierKeyState, const short delta) { // Format for SGR events is: // "\x1b[<%d;%d;%d;%c", xButton, x+1, y+1, fButtonDown? 'M' : 'm' const auto xbutton = _windowsButtonToSGREncoding(button, isHover, modifierKeyState, delta); - - auto format = wil::str_printf(L"\x1b[<%d;%d;%d%c", xbutton, position.x + 1, position.y + 1, isDown ? L'M' : L'm'); - - return format; + return fmt::format(FMT_COMPILE(L"\x1b[<{};{};{}{}"), xbutton, position.x + 1, position.y + 1, isDown ? L'M' : L'm'); } // Routine Description: @@ -552,17 +510,14 @@ bool TerminalInput::ShouldSendAlternateScroll(const unsigned int button, const s // - Sends a sequence to the input corresponding to cursor up / down depending on the sScrollDelta. // Parameters: // - delta: The scroll wheel delta of the input event -// Return value: -// True if the input sequence was sent successfully. -bool TerminalInput::_SendAlternateScroll(const short delta) const noexcept +TerminalInput::OutputType TerminalInput::_makeAlternateScrollOutput(const short delta) const { if (delta > 0) { - _SendInputSequence(_inputMode.test(Mode::CursorKey) ? ApplicationUpSequence : CursorUpSequence); + return MakeOutput(_inputMode.test(Mode::CursorKey) ? ApplicationUpSequence : CursorUpSequence); } else { - _SendInputSequence(_inputMode.test(Mode::CursorKey) ? ApplicationDownSequence : CursorDownSequence); + return MakeOutput(_inputMode.test(Mode::CursorKey) ? ApplicationDownSequence : CursorDownSequence); } - return true; } diff --git a/src/terminal/input/terminalInput.cpp b/src/terminal/input/terminalInput.cpp index 0e17f178574..1a736bd9ed4 100644 --- a/src/terminal/input/terminalInput.cpp +++ b/src/terminal/input/terminalInput.cpp @@ -5,22 +5,12 @@ #include "terminalInput.hpp" #include -#include - -#define WIL_SUPPORT_BITOPERATION_PASCAL_NAMES -#include #include "../../interactivity/inc/VtApiRedirection.hpp" #include "../../inc/unicode.hpp" using namespace Microsoft::Console::VirtualTerminal; -TerminalInput::TerminalInput(_In_ std::function>&)> pfn) : - _leadingSurrogate{} -{ - _pfnWriteEvents = pfn; -} - struct TermKeyMap { const WORD vkey; @@ -361,146 +351,113 @@ static std::optional _searchKeyMapping(const KeyEvent& keyEven return std::nullopt; } -typedef std::function InputSender; - -// Routine Description: -// - Searches the s_modifierKeyMapping for a entry corresponding to this key event. -// Changes the second to last byte to correspond to the currently pressed modifier keys -// before sending to the input. -// Arguments: -// - keyEvent - Key event to translate -// - sender - Function to use to dispatch translated event -// Return Value: -// - True if there was a match to a key translation, and we successfully modified and sent it to the input -static bool _searchWithModifier(const KeyEvent& keyEvent, InputSender sender) +// Searches the s_modifierKeyMapping for a entry corresponding to this key event. +// Changes the second to last byte to correspond to the currently pressed modifier keys. +TerminalInput::OutputType TerminalInput::_searchWithModifier(const KeyEvent& keyEvent) { - auto success = false; - - const auto match = _searchKeyMapping(keyEvent, - { s_modifierKeyMapping.data(), s_modifierKeyMapping.size() }); - if (match) + if (const auto match = _searchKeyMapping(keyEvent, s_modifierKeyMapping)) { const auto& v = match.value(); if (!v.sequence.empty()) { - std::wstring modified{ v.sequence }; // Make a copy so we can modify it. const auto shift = keyEvent.IsShiftPressed(); const auto alt = keyEvent.IsAltPressed(); const auto ctrl = keyEvent.IsCtrlPressed(); - modified.at(modified.size() - 2) = L'1' + (shift ? 1 : 0) + (alt ? 2 : 0) + (ctrl ? 4 : 0); - sender(modified); - success = true; + StringType str{ v.sequence }; + str.at(str.size() - 2) = L'1' + (shift ? 1 : 0) + (alt ? 2 : 0) + (ctrl ? 4 : 0); + return str; } } + + // We didn't find the key in the map of modified keys that need editing, + // maybe it's in the other map of modified keys with sequences that + // don't need editing before sending. + else if (const auto match2 = _searchKeyMapping(keyEvent, s_simpleModifiedKeyMapping)) + { + // This mapping doesn't need to be changed at all. + return MakeOutput(match2->sequence); + } else { - // We didn't find the key in the map of modified keys that need editing, - // maybe it's in the other map of modified keys with sequences that - // don't need editing before sending. - const auto match2 = _searchKeyMapping(keyEvent, - { s_simpleModifiedKeyMapping.data(), s_simpleModifiedKeyMapping.size() }); - if (match2) + // One last check: + // * C-/ is supposed to be ^_ (the C0 character US) + // * C-? is supposed to be DEL + // * C-M-/ is supposed to be ^[^_ + // * C-M-? is supposed to be ^[^? + // + // But this whole scenario is tricky. '/' is not the same VKEY on + // all keyboards. On USASCII keyboards, '/' and '?' share the _same_ + // key. So we have to figure out the vkey at runtime, and we have to + // determine if the key that was pressed was '?' with some + // modifiers, or '/' with some modifiers. + // + // These translations are not in s_simpleModifiedKeyMapping, because + // the aforementioned fact that they aren't the same VKEY on all + // keyboards. + // + // See GH#3079 for details. + // Also see https://github.com/microsoft/terminal/pull/4947#issuecomment-600382856 + + // VkKeyScan will give us both the Vkey of the key needed for this + // character, and the modifiers the user might need to press to get + // this character. + const auto slashKeyScan = OneCoreSafeVkKeyScanW(L'/'); // On USASCII: 0x00bf + const auto questionMarkKeyScan = OneCoreSafeVkKeyScanW(L'?'); //On USASCII: 0x01bf + + const auto slashVkey = LOBYTE(slashKeyScan); + const auto questionMarkVkey = LOBYTE(questionMarkKeyScan); + + const auto ctrl = keyEvent.IsCtrlPressed(); + const auto alt = keyEvent.IsAltPressed(); + const auto shift = keyEvent.IsShiftPressed(); + + // From the KeyEvent we're translating, synthesize the equivalent VkKeyScan result + const auto vkey = keyEvent.GetVirtualKeyCode(); + const short keyScanFromEvent = vkey | + (shift ? 0x100 : 0) | + (ctrl ? 0x200 : 0) | + (alt ? 0x400 : 0); + + // Make sure the VKEY is an _exact_ match, and that the modifier + // bits also match. This handles the hypothetical case we get a + // keyscan back that's ctrl+alt+some_random_VK, and some_random_VK + // has bits that are a superset of the bits set for question mark. + const auto wasQuestionMark = vkey == questionMarkVkey && WI_AreAllFlagsSet(keyScanFromEvent, questionMarkKeyScan); + const auto wasSlash = vkey == slashVkey && WI_AreAllFlagsSet(keyScanFromEvent, slashKeyScan); + + // If the key pressed was exactly the ? key, then try to send the + // appropriate sequence for a modified '?'. Otherwise, check if this + // was a modified '/' keypress. These mappings don't need to be + // changed at all. + if ((ctrl && alt) && wasQuestionMark) { - // This mapping doesn't need to be changed at all. - sender(match2.value().sequence); - success = true; + return MakeOutput(CTRL_ALT_QUESTIONMARK_SEQUENCE); } - else + else if (ctrl && wasQuestionMark) { - // One last check: - // * C-/ is supposed to be ^_ (the C0 character US) - // * C-? is supposed to be DEL - // * C-M-/ is supposed to be ^[^_ - // * C-M-? is supposed to be ^[^? - // - // But this whole scenario is tricky. '/' is not the same VKEY on - // all keyboards. On USASCII keyboards, '/' and '?' share the _same_ - // key. So we have to figure out the vkey at runtime, and we have to - // determine if the key that was pressed was '?' with some - // modifiers, or '/' with some modifiers. - // - // These translations are not in s_simpleModifiedKeyMapping, because - // the aforementioned fact that they aren't the same VKEY on all - // keyboards. - // - // See GH#3079 for details. - // Also see https://github.com/microsoft/terminal/pull/4947#issuecomment-600382856 - - // VkKeyScan will give us both the Vkey of the key needed for this - // character, and the modifiers the user might need to press to get - // this character. - const auto slashKeyScan = OneCoreSafeVkKeyScanW(L'/'); // On USASCII: 0x00bf - const auto questionMarkKeyScan = OneCoreSafeVkKeyScanW(L'?'); //On USASCII: 0x01bf - - const auto slashVkey = LOBYTE(slashKeyScan); - const auto questionMarkVkey = LOBYTE(questionMarkKeyScan); - - const auto ctrl = keyEvent.IsCtrlPressed(); - const auto alt = keyEvent.IsAltPressed(); - const auto shift = keyEvent.IsShiftPressed(); - - // From the KeyEvent we're translating, synthesize the equivalent VkKeyScan result - const auto vkey = keyEvent.GetVirtualKeyCode(); - const short keyScanFromEvent = vkey | - (shift ? 0x100 : 0) | - (ctrl ? 0x200 : 0) | - (alt ? 0x400 : 0); - - // Make sure the VKEY is an _exact_ match, and that the modifier - // bits also match. This handles the hypothetical case we get a - // keyscan back that's ctrl+alt+some_random_VK, and some_random_VK - // has bits that are a superset of the bits set for question mark. - const auto wasQuestionMark = vkey == questionMarkVkey && WI_AreAllFlagsSet(keyScanFromEvent, questionMarkKeyScan); - const auto wasSlash = vkey == slashVkey && WI_AreAllFlagsSet(keyScanFromEvent, slashKeyScan); - - // If the key pressed was exactly the ? key, then try to send the - // appropriate sequence for a modified '?'. Otherwise, check if this - // was a modified '/' keypress. These mappings don't need to be - // changed at all. - if ((ctrl && alt) && wasQuestionMark) - { - sender(CTRL_ALT_QUESTIONMARK_SEQUENCE); - success = true; - } - else if (ctrl && wasQuestionMark) - { - sender(CTRL_QUESTIONMARK_SEQUENCE); - success = true; - } - else if ((ctrl && alt) && wasSlash) - { - sender(CTRL_ALT_SLASH_SEQUENCE); - success = true; - } - else if (ctrl && wasSlash) - { - sender(CTRL_SLASH_SEQUENCE); - success = true; - } + return MakeOutput(CTRL_QUESTIONMARK_SEQUENCE); + } + else if ((ctrl && alt) && wasSlash) + { + return MakeOutput(CTRL_ALT_SLASH_SEQUENCE); + } + else if (ctrl && wasSlash) + { + return MakeOutput(CTRL_SLASH_SEQUENCE); } } - return success; + return MakeUnhandled(); } -// Routine Description: -// - Searches the input array of mappings, and sends it to the input if a match was found. -// Arguments: -// - keyEvent - Key event to translate -// - keyMapping - Array of key mappings to search -// - sender - Function to use to dispatch translated event -// Return Value: -// - True if there was a match to a key translation, and we successfully sent it to the input -static bool _translateDefaultMapping(const KeyEvent& keyEvent, - const std::span keyMapping, - InputSender sender) +TerminalInput::OutputType TerminalInput::MakeUnhandled() noexcept { - const auto match = _searchKeyMapping(keyEvent, keyMapping); - if (match) - { - sender(match->sequence); - } - return match.has_value(); + return {}; +} + +TerminalInput::OutputType TerminalInput::MakeOutput(const std::wstring_view& str) +{ + return { StringType{ str } }; } // Routine Description: @@ -515,34 +472,19 @@ static bool _translateDefaultMapping(const KeyEvent& keyEvent, // Arguments: // - keyEvent - Key event to translate // Return Value: -// - True if the event was handled. -bool TerminalInput::HandleKey(const IInputEvent* const pInEvent) +// - Returns an empty optional if we didn't handle the key event and the caller can opt to handle it in some other way. +// - Returns a string if we successfully translated it into a VT input sequence. +TerminalInput::OutputType TerminalInput::HandleKey(const IInputEvent* const pInEvent) { if (!pInEvent) { - return false; - } - - // GH#11682: If this was a focus event, we can handle this. Steal the - // focused state, and return true if we're actually in focus event mode. - if (pInEvent->EventType() == InputEventType::FocusEvent) - { - const auto& focusEvent = *static_cast(pInEvent); - - // BODGY - // GH#13238 - Filter out focus events that came from the API. - if (focusEvent.CameFromApi()) - { - return false; - } - - return HandleFocus(focusEvent.GetFocus()); + return MakeUnhandled(); } // On key presses, prepare to translate to VT compatible sequences if (pInEvent->EventType() != InputEventType::KeyEvent) { - return false; + return MakeUnhandled(); } auto keyEvent = *static_cast(pInEvent); @@ -552,9 +494,7 @@ bool TerminalInput::HandleKey(const IInputEvent* const pInEvent) // Only do this if win32-input-mode support isn't manually disabled. if (_inputMode.test(Mode::Win32) && !_forceDisableWin32InputMode) { - const auto seq = _GenerateWin32KeySequence(keyEvent); - _SendInputSequence(seq); - return true; + return _makeWin32Output(keyEvent); } // Check if this key matches the last recorded key code. @@ -568,16 +508,16 @@ bool TerminalInput::HandleKey(const IInputEvent* const pInEvent) { _lastVirtualKeyCode = std::nullopt; } - return false; + return MakeUnhandled(); } // If this is a repeat of the last recorded key press, and Auto Repeat Mode // is disabled, then we should suppress this event. if (matchingLastKeyPress && !_inputMode.test(Mode::AutoRepeat)) { - // Note that we must return true here to say we've handled the event, - // otherwise the key press can still end up being submitted. - return true; + // Note that we must return an empty string here to imply that we've handled + // the event, otherwise the key press can still end up being submitted. + return MakeOutput({}); } _lastVirtualKeyCode = keyEvent.GetVirtualKeyCode(); @@ -591,13 +531,12 @@ bool TerminalInput::HandleKey(const IInputEvent* const pInEvent) // The Alt modifier adds an escape prefix. if (keyEvent.IsAltPressed()) { - _SendEscapedInputSequence(seq); + return _makeEscapedOutput(seq); } else { - _SendInputSequence({ &seq, 1 }); + return MakeOutput({ &seq, 1 }); } - return true; } // When the Line Feed mode is set, a VK_RETURN key should send both CR and LF. @@ -605,8 +544,7 @@ bool TerminalInput::HandleKey(const IInputEvent* const pInEvent) // CR, or when the Ctrl modifier is pressed, just LF. if (keyEvent.GetVirtualKeyCode() == VK_RETURN && _inputMode.test(Mode::LineFeed)) { - _SendInputSequence(L"\r\n"); - return true; + return MakeOutput(L"\r\n"); } // Many keyboard layouts have an AltGr key, which makes widely used characters accessible. @@ -646,8 +584,7 @@ bool TerminalInput::HandleKey(const IInputEvent* const pInEvent) { // Pressing the control key causes all bits but the 5 least // significant ones to be zeroed out (when using ASCII). - _SendEscapedInputSequence(ctrlAltChar & 0b11111); - return true; + return _makeEscapedOutput(ctrlAltChar & 0b11111); } // Currently, when we're called with Alt+Ctrl+@, ch will be 0, since Ctrl+@ equals a null byte. @@ -655,27 +592,24 @@ bool TerminalInput::HandleKey(const IInputEvent* const pInEvent) // -> Use the vkey to determine if Ctrl+@ is being pressed and produce ^[^@. if (ch == UNICODE_NULL && vkey == LOBYTE(OneCoreSafeVkKeyScanW(0))) { - _SendEscapedInputSequence(L'\0'); - return true; + return _makeEscapedOutput(L'\0'); } } - const auto senderFunc = [this](const std::wstring_view seq) noexcept { - _SendInputSequence(seq); - }; - // If a modifier key was pressed, then we need to try and send the modified sequence. - if (keyEvent.IsModifierPressed() && _searchWithModifier(keyEvent, senderFunc)) + if (keyEvent.IsModifierPressed()) { - return true; + if (auto out = _searchWithModifier(keyEvent)) + { + return out; + } } // This section is similar to the Alt modifier section above, // but handles cases without Ctrl modifiers. if (keyEvent.IsAltPressed() && !keyEvent.IsCtrlPressed() && keyEvent.GetCharData() != 0) { - _SendEscapedInputSequence(keyEvent.GetCharData()); - return true; + return _makeEscapedOutput(keyEvent.GetCharData()); } // Pressing the control key causes all bits but the 5 least @@ -696,8 +630,7 @@ bool TerminalInput::HandleKey(const IInputEvent* const pInEvent) // -> Use the vkey to alternatively determine if Ctrl+@ is being pressed. if (ch == UNICODE_SPACE || (ch == UNICODE_NULL && vkey == LOBYTE(OneCoreSafeVkKeyScanW(0)))) { - _SendNullInputSequence(keyEvent.GetActiveModifierKeys()); - return true; + return _makeCharOutput(0); } // Not all keyboard layouts contain mappings for Ctrl-key combinations. @@ -712,8 +645,7 @@ bool TerminalInput::HandleKey(const IInputEvent* const pInEvent) // Pressing the control key causes all bits but the 5 least // significant ones to be zeroed out (when using ASCII). mappedChar &= 0b11111; - _SendChar(mappedChar); - return true; + return _makeCharOutput(mappedChar); } } } @@ -722,131 +654,83 @@ bool TerminalInput::HandleKey(const IInputEvent* const pInEvent) // These mappings will kick in no matter which modifiers are pressed and as such // must be checked last, or otherwise we'd override more complex key combinations. const auto mapping = _getKeyMapping(keyEvent, _inputMode.test(Mode::Ansi), _inputMode.test(Mode::CursorKey), _inputMode.test(Mode::Keypad)); - if (_translateDefaultMapping(keyEvent, mapping, senderFunc)) + if (const auto match = _searchKeyMapping(keyEvent, mapping)) { - return true; + return MakeOutput(match->sequence); } // If all else fails we can finally try to send the character itself if there is any. if (keyEvent.GetCharData() != 0) { - _SendChar(keyEvent.GetCharData()); - return true; + return _makeCharOutput(keyEvent.GetCharData()); } - return false; + return MakeUnhandled(); } -bool TerminalInput::HandleFocus(const bool focused) noexcept +TerminalInput::OutputType TerminalInput::HandleFocus(const bool focused) const { - const auto enabled{ _inputMode.test(Mode::FocusEvent) }; - if (enabled) + if (!_inputMode.test(Mode::FocusEvent)) { - _SendInputSequence(focused ? L"\x1b[I" : L"\x1b[O"); + return MakeUnhandled(); } - return enabled; + + return MakeOutput(focused ? L"\x1b[I" : L"\x1b[O"); } -// Routine Description: -// - Sends the given character to the shell. -// - Surrogate pairs are being aggregated by this function before being sent. -// Arguments: -// - ch: The UTF-16 character to send. -void TerminalInput::_SendChar(const wchar_t ch) +// Turns the given character into OutputType. +// If it encounters a surrogate pair, it'll buffer the leading character until a +// trailing one has been received and then flush both of them simultaneously. +// Surrogate pairs should always be handled as proper pairs after all. +TerminalInput::OutputType TerminalInput::_makeCharOutput(const wchar_t ch) { + StringType str; + if (til::is_leading_surrogate(ch)) { - if (_leadingSurrogate.has_value()) - { - // we already were storing a leading surrogate but we got another one. Go ahead and send the - // saved surrogate piece and save the new one - const auto formatted = wil::str_printf(L"%I32u", _leadingSurrogate.value()); - _SendInputSequence(formatted); - } - // save the leading portion of a surrogate pair so that they can be sent at the same time _leadingSurrogate.emplace(ch); } - else if (_leadingSurrogate.has_value()) + else if (_leadingSurrogate) { - std::array wstr{ { _leadingSurrogate.value(), ch } }; + const auto lead = *_leadingSurrogate; _leadingSurrogate.reset(); - _SendInputSequence({ wstr.data(), wstr.size() }); + + if (til::is_trailing_surrogate(ch)) + { + str.push_back(lead); + str.push_back(ch); + } } else { - _SendInputSequence({ &ch, 1 }); + str.push_back(ch); } -} -// Routine Description: -// - Sends the given char as a sequence representing Alt+wch, also the same as -// Meta+wch. -// Arguments: -// - wch - character to send to input paired with Esc -// Return Value: -// - None -void TerminalInput::_SendEscapedInputSequence(const wchar_t wch) const -{ - try - { - std::deque> inputEvents; - inputEvents.push_back(std::make_unique(true, 1ui16, 0ui16, 0ui16, L'\x1b', 0)); - inputEvents.push_back(std::make_unique(true, 1ui16, 0ui16, 0ui16, wch, 0)); - _pfnWriteEvents(inputEvents); - } - catch (...) - { - LOG_HR(wil::ResultFromCaughtException()); - } + return str; } -void TerminalInput::_SendNullInputSequence(const DWORD controlKeyState) const +// Sends the given char as a sequence representing Alt+wch, also the same as Meta+wch. +TerminalInput::OutputType TerminalInput::_makeEscapedOutput(const wchar_t wch) { - try - { - std::deque> inputEvents; - inputEvents.push_back(std::make_unique(true, - 1ui16, - LOBYTE(OneCoreSafeVkKeyScanW(0)), - 0ui16, - L'\x0', - controlKeyState)); - _pfnWriteEvents(inputEvents); - } - catch (...) - { - LOG_HR(wil::ResultFromCaughtException()); - } + StringType str; + str.push_back(L'\x1b'); + str.push_back(wch); + return str; } -void TerminalInput::_SendInputSequence(const std::wstring_view sequence) const noexcept +// Turns an KEY_EVENT_RECORD into a win32-input-mode VT sequence. +// It allows us to send KEY_EVENT_RECORD data losslessly to conhost. +TerminalInput::OutputType TerminalInput::_makeWin32Output(const KeyEvent& key) { - if (!sequence.empty()) - { - try - { - std::deque> inputEvents; - for (const auto& wch : sequence) - { - inputEvents.push_back(std::make_unique(true, 1ui16, 0ui16, 0ui16, wch, 0)); - } - _pfnWriteEvents(inputEvents); - } - catch (...) - { - LOG_HR(wil::ResultFromCaughtException()); - } - } -} + // .uChar.UnicodeChar must be cast to an integer because we want its numerical value. + // Casting the rest to uint16_t as well doesn't hurt because that's MAX_PARAMETER_VALUE anyways. + const auto kd = gsl::narrow_cast(key.IsKeyDown() ? 1 : 0); + const auto rc = gsl::narrow_cast(key.GetRepeatCount()); + const auto vk = gsl::narrow_cast(key.GetVirtualKeyCode()); + const auto sc = gsl::narrow_cast(key.GetVirtualScanCode()); + const auto uc = gsl::narrow_cast(key.GetCharData()); + const auto cs = gsl::narrow_cast(key.GetActiveModifierKeys()); -// Method Description: -// - Synthesize a win32-input-mode sequence for the given keyevent. -// Arguments: -// - key: the KeyEvent to serialize. -// Return Value: -// - the formatted string representation of this key -std::wstring TerminalInput::_GenerateWin32KeySequence(const KeyEvent& key) -{ // Sequences are formatted as follows: // // ^[ [ Vk ; Sc ; Uc ; Kd ; Cs ; Rc _ @@ -858,11 +742,5 @@ std::wstring TerminalInput::_GenerateWin32KeySequence(const KeyEvent& key) // Kd: the value of bKeyDown - either a '0' or '1'. If omitted, defaults to '0'. // Cs: the value of dwControlKeyState - any number. If omitted, defaults to '0'. // Rc: the value of wRepeatCount - any number. If omitted, defaults to '1'. - return fmt::format(FMT_COMPILE(L"\x1b[{};{};{};{};{};{}_"), - key.GetVirtualKeyCode(), - key.GetVirtualScanCode(), - static_cast(key.GetCharData()), - key.IsKeyDown() ? 1 : 0, - key.GetActiveModifierKeys(), - key.GetRepeatCount()); + return fmt::format(FMT_COMPILE(L"\x1b[{};{};{};{};{};{}_"), vk, sc, uc, kd, cs, rc); } diff --git a/src/terminal/input/terminalInput.hpp b/src/terminal/input/terminalInput.hpp index da88bdad979..8fcabe48987 100644 --- a/src/terminal/input/terminalInput.hpp +++ b/src/terminal/input/terminalInput.hpp @@ -1,4 +1,4 @@ -/*++ +/*+ Copyright (c) Microsoft Corporation Licensed under the MIT license. @@ -13,28 +13,30 @@ Author(s): - Michael Niksa (MiNiksa) 30-Oct-2015 --*/ -#include -#include "../../types/inc/IInputEvent.hpp" #pragma once +#include "../../types/inc/IInputEvent.hpp" + namespace Microsoft::Console::VirtualTerminal { class TerminalInput final { public: - TerminalInput(_In_ std::function>&)> pfn); + using StringType = std::wstring; + using OutputType = std::optional; - TerminalInput() = delete; - TerminalInput(const TerminalInput& old) = default; - TerminalInput(TerminalInput&& moved) = default; - - TerminalInput& operator=(const TerminalInput& old) = default; - TerminalInput& operator=(TerminalInput&& moved) = default; - - ~TerminalInput() = default; + struct MouseButtonState + { + bool isLeftButtonDown; + bool isMiddleButtonDown; + bool isRightButtonDown; + }; - bool HandleKey(const IInputEvent* const pInEvent); - bool HandleFocus(const bool focused) noexcept; + static [[nodiscard]] OutputType MakeUnhandled() noexcept; + static [[nodiscard]] OutputType MakeOutput(const std::wstring_view& str); + [[nodiscard]] OutputType HandleKey(const IInputEvent* const pInEvent); + [[nodiscard]] OutputType HandleFocus(bool focused) const; + [[nodiscard]] OutputType HandleMouse(til::point position, unsigned int button, short modifierKeyState, short delta, MouseButtonState state); enum class Mode : size_t { @@ -66,19 +68,6 @@ namespace Microsoft::Console::VirtualTerminal #pragma region MouseInput // These methods are defined in mouseInput.cpp - struct MouseButtonState - { - bool isLeftButtonDown; - bool isMiddleButtonDown; - bool isRightButtonDown; - }; - - bool HandleMouse(const til::point position, - const unsigned int button, - const short modifierKeyState, - const short delta, - const MouseButtonState state); - bool IsTrackingMouseInput() const noexcept; bool ShouldSendAlternateScroll(const unsigned int button, const short delta) const noexcept; #pragma endregion @@ -90,8 +79,6 @@ namespace Microsoft::Console::VirtualTerminal #pragma endregion private: - std::function>&)> _pfnWriteEvents; - // storage location for the leading surrogate of a utf-16 surrogate pair std::optional _leadingSurrogate; @@ -100,11 +87,10 @@ namespace Microsoft::Console::VirtualTerminal til::enumset _inputMode{ Mode::Ansi, Mode::AutoRepeat }; bool _forceDisableWin32InputMode{ false }; - void _SendChar(const wchar_t ch); - void _SendNullInputSequence(const DWORD dwControlKeyState) const; - void _SendInputSequence(const std::wstring_view sequence) const noexcept; - void _SendEscapedInputSequence(const wchar_t wch) const; - static std::wstring _GenerateWin32KeySequence(const KeyEvent& key); + [[nodiscard]] OutputType _makeCharOutput(wchar_t ch); + static [[nodiscard]] OutputType _makeEscapedOutput(wchar_t wch); + static [[nodiscard]] OutputType _makeWin32Output(const KeyEvent& key); + static [[nodiscard]] OutputType _searchWithModifier(const KeyEvent& keyEvent); #pragma region MouseInputState Management // These methods are defined in mouseInputState.cpp @@ -120,24 +106,11 @@ namespace Microsoft::Console::VirtualTerminal #pragma endregion #pragma region MouseInput - static std::wstring _GenerateDefaultSequence(const til::point position, - const unsigned int button, - const bool isHover, - const short modifierKeyState, - const short delta); - static std::wstring _GenerateUtf8Sequence(const til::point position, - const unsigned int button, - const bool isHover, - const short modifierKeyState, - const short delta); - static std::wstring _GenerateSGRSequence(const til::point position, - const unsigned int button, - const bool isDown, - const bool isHover, - const short modifierKeyState, - const short delta); - - bool _SendAlternateScroll(const short delta) const noexcept; + static [[nodiscard]] OutputType _GenerateDefaultSequence(til::point position, unsigned int button, bool isHover, short modifierKeyState, short delta); + static [[nodiscard]] OutputType _GenerateUtf8Sequence(til::point position, unsigned int button, bool isHover, short modifierKeyState, short delta); + static [[nodiscard]] OutputType _GenerateSGRSequence(til::point position, unsigned int button, bool isDown, bool isHover, short modifierKeyState, short delta); + + [[nodiscard]] OutputType _makeAlternateScrollOutput(short delta) const; static constexpr unsigned int s_GetPressedButton(const MouseButtonState state) noexcept; #pragma endregion diff --git a/src/terminal/parser/InputStateMachineEngine.hpp b/src/terminal/parser/InputStateMachineEngine.hpp index 0bcc8398ebf..a2ab34623fd 100644 --- a/src/terminal/parser/InputStateMachineEngine.hpp +++ b/src/terminal/parser/InputStateMachineEngine.hpp @@ -15,7 +15,6 @@ Author(s): --*/ #pragma once -#include "telemetry.hpp" #include "IStateMachineEngine.hpp" #include #include "../../types/inc/IInputEvent.hpp" diff --git a/src/terminal/parser/OutputStateMachineEngine.cpp b/src/terminal/parser/OutputStateMachineEngine.cpp index 07d549e1c9a..c08c283cd06 100644 --- a/src/terminal/parser/OutputStateMachineEngine.cpp +++ b/src/terminal/parser/OutputStateMachineEngine.cpp @@ -211,103 +211,78 @@ bool OutputStateMachineEngine::ActionEscDispatch(const VTID id) break; case EscActionCodes::DECBI_BackIndex: success = _dispatch->BackIndex(); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECBI); break; case EscActionCodes::DECSC_CursorSave: success = _dispatch->CursorSaveState(); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSC); break; case EscActionCodes::DECRC_CursorRestore: success = _dispatch->CursorRestoreState(); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECRC); break; case EscActionCodes::DECFI_ForwardIndex: success = _dispatch->ForwardIndex(); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECFI); break; case EscActionCodes::DECKPAM_KeypadApplicationMode: success = _dispatch->SetKeypadMode(true); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECKPAM); break; case EscActionCodes::DECKPNM_KeypadNumericMode: success = _dispatch->SetKeypadMode(false); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECKPNM); break; case EscActionCodes::NEL_NextLine: success = _dispatch->LineFeed(DispatchTypes::LineFeedType::WithReturn); - TermTelemetry::Instance().Log(TermTelemetry::Codes::NEL); break; case EscActionCodes::IND_Index: success = _dispatch->LineFeed(DispatchTypes::LineFeedType::WithoutReturn); - TermTelemetry::Instance().Log(TermTelemetry::Codes::IND); break; case EscActionCodes::RI_ReverseLineFeed: success = _dispatch->ReverseLineFeed(); - TermTelemetry::Instance().Log(TermTelemetry::Codes::RI); break; case EscActionCodes::HTS_HorizontalTabSet: success = _dispatch->HorizontalTabSet(); - TermTelemetry::Instance().Log(TermTelemetry::Codes::HTS); break; case EscActionCodes::DECID_IdentifyDevice: success = _dispatch->DeviceAttributes(); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DA); break; case EscActionCodes::RIS_ResetToInitialState: success = _dispatch->HardReset(); - TermTelemetry::Instance().Log(TermTelemetry::Codes::RIS); break; case EscActionCodes::SS2_SingleShift: success = _dispatch->SingleShift(2); - TermTelemetry::Instance().Log(TermTelemetry::Codes::SS2); break; case EscActionCodes::SS3_SingleShift: success = _dispatch->SingleShift(3); - TermTelemetry::Instance().Log(TermTelemetry::Codes::SS3); break; case EscActionCodes::LS2_LockingShift: success = _dispatch->LockingShift(2); - TermTelemetry::Instance().Log(TermTelemetry::Codes::LS2); break; case EscActionCodes::LS3_LockingShift: success = _dispatch->LockingShift(3); - TermTelemetry::Instance().Log(TermTelemetry::Codes::LS3); break; case EscActionCodes::LS1R_LockingShift: success = _dispatch->LockingShiftRight(1); - TermTelemetry::Instance().Log(TermTelemetry::Codes::LS1R); break; case EscActionCodes::LS2R_LockingShift: success = _dispatch->LockingShiftRight(2); - TermTelemetry::Instance().Log(TermTelemetry::Codes::LS2R); break; case EscActionCodes::LS3R_LockingShift: success = _dispatch->LockingShiftRight(3); - TermTelemetry::Instance().Log(TermTelemetry::Codes::LS3R); break; case EscActionCodes::DECAC1_AcceptC1Controls: success = _dispatch->AcceptC1Controls(true); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECAC1); break; case EscActionCodes::DECDHL_DoubleHeightLineTop: success = _dispatch->SetLineRendition(LineRendition::DoubleHeightTop); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECDHL); break; case EscActionCodes::DECDHL_DoubleHeightLineBottom: success = _dispatch->SetLineRendition(LineRendition::DoubleHeightBottom); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECDHL); break; case EscActionCodes::DECSWL_SingleWidthLine: success = _dispatch->SetLineRendition(LineRendition::SingleWidth); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSWL); break; case EscActionCodes::DECDWL_DoubleWidthLine: success = _dispatch->SetLineRendition(LineRendition::DoubleWidth); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECDWL); break; case EscActionCodes::DECALN_ScreenAlignmentPattern: success = _dispatch->ScreenAlignmentPattern(); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECALN); break; default: const auto commandChar = id[0]; @@ -316,35 +291,27 @@ bool OutputStateMachineEngine::ActionEscDispatch(const VTID id) { case '%': success = _dispatch->DesignateCodingSystem(commandParameter); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DOCS); break; case '(': success = _dispatch->Designate94Charset(0, commandParameter); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DesignateG0); break; case ')': success = _dispatch->Designate94Charset(1, commandParameter); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DesignateG1); break; case '*': success = _dispatch->Designate94Charset(2, commandParameter); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DesignateG2); break; case '+': success = _dispatch->Designate94Charset(3, commandParameter); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DesignateG3); break; case '-': success = _dispatch->Designate96Charset(1, commandParameter); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DesignateG1); break; case '.': success = _dispatch->Designate96Charset(2, commandParameter); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DesignateG2); break; case '/': success = _dispatch->Designate96Charset(3, commandParameter); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DesignateG3); break; default: // If no functions to call, overall dispatch was a failure. @@ -455,185 +422,144 @@ bool OutputStateMachineEngine::ActionCsiDispatch(const VTID id, const VTParamete { case CsiActionCodes::CUU_CursorUp: success = _dispatch->CursorUp(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::CUU); break; case CsiActionCodes::CUD_CursorDown: success = _dispatch->CursorDown(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::CUD); break; case CsiActionCodes::CUF_CursorForward: success = _dispatch->CursorForward(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::CUF); break; case CsiActionCodes::CUB_CursorBackward: success = _dispatch->CursorBackward(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::CUB); break; case CsiActionCodes::CNL_CursorNextLine: success = _dispatch->CursorNextLine(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::CNL); break; case CsiActionCodes::CPL_CursorPrevLine: success = _dispatch->CursorPrevLine(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::CPL); break; case CsiActionCodes::CHA_CursorHorizontalAbsolute: case CsiActionCodes::HPA_HorizontalPositionAbsolute: success = _dispatch->CursorHorizontalPositionAbsolute(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::CHA); break; case CsiActionCodes::VPA_VerticalLinePositionAbsolute: success = _dispatch->VerticalLinePositionAbsolute(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::VPA); break; case CsiActionCodes::HPR_HorizontalPositionRelative: success = _dispatch->HorizontalPositionRelative(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::HPR); break; case CsiActionCodes::VPR_VerticalPositionRelative: success = _dispatch->VerticalPositionRelative(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::VPR); break; case CsiActionCodes::CUP_CursorPosition: case CsiActionCodes::HVP_HorizontalVerticalPosition: success = _dispatch->CursorPosition(parameters.at(0), parameters.at(1)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::CUP); break; case CsiActionCodes::DECSTBM_SetTopBottomMargins: success = _dispatch->SetTopBottomScrollingMargins(parameters.at(0).value_or(0), parameters.at(1).value_or(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSTBM); break; case CsiActionCodes::DECSLRM_SetLeftRightMargins: // Note that this can also be ANSISYSSC, depending on the state of DECLRMM. success = _dispatch->SetLeftRightScrollingMargins(parameters.at(0).value_or(0), parameters.at(1).value_or(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSTBM); break; case CsiActionCodes::ICH_InsertCharacter: success = _dispatch->InsertCharacter(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::ICH); break; case CsiActionCodes::DCH_DeleteCharacter: success = _dispatch->DeleteCharacter(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DCH); break; case CsiActionCodes::ED_EraseDisplay: success = parameters.for_each([&](const auto eraseType) { return _dispatch->EraseInDisplay(eraseType); }); - TermTelemetry::Instance().Log(TermTelemetry::Codes::ED); break; case CsiActionCodes::DECSED_SelectiveEraseDisplay: success = parameters.for_each([&](const auto eraseType) { return _dispatch->SelectiveEraseInDisplay(eraseType); }); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSED); break; case CsiActionCodes::EL_EraseLine: success = parameters.for_each([&](const auto eraseType) { return _dispatch->EraseInLine(eraseType); }); - TermTelemetry::Instance().Log(TermTelemetry::Codes::EL); break; case CsiActionCodes::DECSEL_SelectiveEraseLine: success = parameters.for_each([&](const auto eraseType) { return _dispatch->SelectiveEraseInLine(eraseType); }); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSEL); break; case CsiActionCodes::SM_SetMode: success = parameters.for_each([&](const auto mode) { return _dispatch->SetMode(DispatchTypes::ANSIStandardMode(mode)); }); - TermTelemetry::Instance().Log(TermTelemetry::Codes::SM); break; case CsiActionCodes::DECSET_PrivateModeSet: success = parameters.for_each([&](const auto mode) { return _dispatch->SetMode(DispatchTypes::DECPrivateMode(mode)); }); - //TODO: MSFT:6367459 Add specific logging for each of the DECSET/DECRST codes - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSET); break; case CsiActionCodes::RM_ResetMode: success = parameters.for_each([&](const auto mode) { return _dispatch->ResetMode(DispatchTypes::ANSIStandardMode(mode)); }); - TermTelemetry::Instance().Log(TermTelemetry::Codes::RM); break; case CsiActionCodes::DECRST_PrivateModeReset: success = parameters.for_each([&](const auto mode) { return _dispatch->ResetMode(DispatchTypes::DECPrivateMode(mode)); }); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECRST); break; case CsiActionCodes::SGR_SetGraphicsRendition: success = _dispatch->SetGraphicsRendition(parameters); - TermTelemetry::Instance().Log(TermTelemetry::Codes::SGR); break; case CsiActionCodes::DSR_DeviceStatusReport: success = _dispatch->DeviceStatusReport(DispatchTypes::ANSIStandardStatus(parameters.at(0)), parameters.at(1)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DSR); break; case CsiActionCodes::DSR_PrivateDeviceStatusReport: success = _dispatch->DeviceStatusReport(DispatchTypes::DECPrivateStatus(parameters.at(0)), parameters.at(1)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DSR); break; case CsiActionCodes::DA_DeviceAttributes: success = parameters.at(0).value_or(0) == 0 && _dispatch->DeviceAttributes(); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DA); break; case CsiActionCodes::DA2_SecondaryDeviceAttributes: success = parameters.at(0).value_or(0) == 0 && _dispatch->SecondaryDeviceAttributes(); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DA2); break; case CsiActionCodes::DA3_TertiaryDeviceAttributes: success = parameters.at(0).value_or(0) == 0 && _dispatch->TertiaryDeviceAttributes(); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DA3); break; case CsiActionCodes::DECREQTPARM_RequestTerminalParameters: success = _dispatch->RequestTerminalParameters(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECREQTPARM); break; case CsiActionCodes::SU_ScrollUp: success = _dispatch->ScrollUp(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::SU); break; case CsiActionCodes::SD_ScrollDown: success = _dispatch->ScrollDown(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::SD); break; case CsiActionCodes::ANSISYSRC_CursorRestore: success = _dispatch->CursorRestoreState(); - TermTelemetry::Instance().Log(TermTelemetry::Codes::ANSISYSRC); break; case CsiActionCodes::IL_InsertLine: success = _dispatch->InsertLine(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::IL); break; case CsiActionCodes::DL_DeleteLine: success = _dispatch->DeleteLine(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DL); break; case CsiActionCodes::CHT_CursorForwardTab: success = _dispatch->ForwardTab(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::CHT); break; case CsiActionCodes::CBT_CursorBackTab: success = _dispatch->BackwardsTab(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::CBT); break; case CsiActionCodes::TBC_TabClear: success = parameters.for_each([&](const auto clearType) { return _dispatch->TabClear(clearType); }); - TermTelemetry::Instance().Log(TermTelemetry::Codes::TBC); break; case CsiActionCodes::ECH_EraseCharacters: success = _dispatch->EraseCharacters(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::ECH); break; case CsiActionCodes::DTTERM_WindowManipulation: success = _dispatch->WindowManipulation(parameters.at(0), parameters.at(1), parameters.at(2)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DTTERM_WM); break; case CsiActionCodes::REP_RepeatCharacter: // Handled w/o the dispatch. This function is unique in that way @@ -648,93 +574,71 @@ bool OutputStateMachineEngine::ActionCsiDispatch(const VTID id, const VTParamete _dispatch->PrintString(wstr); } success = true; - TermTelemetry::Instance().Log(TermTelemetry::Codes::REP); break; case CsiActionCodes::DECSCUSR_SetCursorStyle: success = _dispatch->SetCursorStyle(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSCUSR); break; case CsiActionCodes::DECSTR_SoftReset: success = _dispatch->SoftReset(); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSTR); break; case CsiActionCodes::DECSCA_SetCharacterProtectionAttribute: success = _dispatch->SetCharacterProtectionAttribute(parameters); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSCA); break; case CsiActionCodes::XT_PushSgr: case CsiActionCodes::XT_PushSgrAlias: success = _dispatch->PushGraphicsRendition(parameters); - TermTelemetry::Instance().Log(TermTelemetry::Codes::XTPUSHSGR); break; case CsiActionCodes::XT_PopSgr: case CsiActionCodes::XT_PopSgrAlias: success = _dispatch->PopGraphicsRendition(); - TermTelemetry::Instance().Log(TermTelemetry::Codes::XTPOPSGR); break; case CsiActionCodes::DECRQM_RequestMode: success = _dispatch->RequestMode(DispatchTypes::ANSIStandardMode(parameters.at(0))); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECRQM); break; case CsiActionCodes::DECRQM_PrivateRequestMode: success = _dispatch->RequestMode(DispatchTypes::DECPrivateMode(parameters.at(0))); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECRQM); break; case CsiActionCodes::DECCARA_ChangeAttributesRectangularArea: success = _dispatch->ChangeAttributesRectangularArea(parameters.at(0), parameters.at(1), parameters.at(2).value_or(0), parameters.at(3).value_or(0), parameters.subspan(4)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECCARA); break; case CsiActionCodes::DECRARA_ReverseAttributesRectangularArea: success = _dispatch->ReverseAttributesRectangularArea(parameters.at(0), parameters.at(1), parameters.at(2).value_or(0), parameters.at(3).value_or(0), parameters.subspan(4)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECRARA); break; case CsiActionCodes::DECCRA_CopyRectangularArea: success = _dispatch->CopyRectangularArea(parameters.at(0), parameters.at(1), parameters.at(2).value_or(0), parameters.at(3).value_or(0), parameters.at(4), parameters.at(5), parameters.at(6), parameters.at(7)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECCRA); break; case CsiActionCodes::DECRQPSR_RequestPresentationStateReport: success = _dispatch->RequestPresentationStateReport(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECRQPSR); break; case CsiActionCodes::DECFRA_FillRectangularArea: success = _dispatch->FillRectangularArea(parameters.at(0), parameters.at(1), parameters.at(2), parameters.at(3).value_or(0), parameters.at(4).value_or(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECFRA); break; case CsiActionCodes::DECERA_EraseRectangularArea: success = _dispatch->EraseRectangularArea(parameters.at(0), parameters.at(1), parameters.at(2).value_or(0), parameters.at(3).value_or(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECERA); break; case CsiActionCodes::DECSERA_SelectiveEraseRectangularArea: success = _dispatch->SelectiveEraseRectangularArea(parameters.at(0), parameters.at(1), parameters.at(2).value_or(0), parameters.at(3).value_or(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSERA); break; case CsiActionCodes::DECIC_InsertColumn: success = _dispatch->InsertColumn(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECIC); break; case CsiActionCodes::DECDC_DeleteColumn: success = _dispatch->DeleteColumn(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECDC); break; case CsiActionCodes::DECSACE_SelectAttributeChangeExtent: success = _dispatch->SelectAttributeChangeExtent(parameters.at(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSACE); break; case CsiActionCodes::DECRQCRA_RequestChecksumRectangularArea: success = _dispatch->RequestChecksumRectangularArea(parameters.at(0).value_or(0), parameters.at(1).value_or(0), parameters.at(2), parameters.at(3), parameters.at(4).value_or(0), parameters.at(5).value_or(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECRQCRA); break; case CsiActionCodes::DECINVM_InvokeMacro: success = _dispatch->InvokeMacro(parameters.at(0).value_or(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECINVM); break; case CsiActionCodes::DECAC_AssignColor: success = _dispatch->AssignColor(parameters.at(0), parameters.at(1).value_or(0), parameters.at(2).value_or(0)); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECAC); break; case CsiActionCodes::DECPS_PlaySound: success = _dispatch->PlaySounds(parameters); - TermTelemetry::Instance().Log(TermTelemetry::Codes::DECPS); break; default: // If no functions to call, overall dispatch was a failure. @@ -851,7 +755,6 @@ bool OutputStateMachineEngine::ActionOscDispatch(const wchar_t /*wch*/, std::wstring title; success = _GetOscTitle(string, title); success = success && _dispatch->SetWindowTitle(title); - TermTelemetry::Instance().Log(TermTelemetry::Codes::OSCWT); break; } case OscActionCodes::SetColor: @@ -865,7 +768,6 @@ bool OutputStateMachineEngine::ActionOscDispatch(const wchar_t /*wch*/, const auto rgb = til::at(colors, i); success = success && _dispatch->SetColorTableEntry(tableIndex, rgb); } - TermTelemetry::Instance().Log(TermTelemetry::Codes::OSCCT); break; } case OscActionCodes::SetForegroundColor: @@ -886,7 +788,6 @@ bool OutputStateMachineEngine::ActionOscDispatch(const wchar_t /*wch*/, { success = success && _dispatch->SetDefaultForeground(color); } - TermTelemetry::Instance().Log(TermTelemetry::Codes::OSCFG); commandIndex++; colorIndex++; } @@ -898,7 +799,6 @@ bool OutputStateMachineEngine::ActionOscDispatch(const wchar_t /*wch*/, { success = success && _dispatch->SetDefaultBackground(color); } - TermTelemetry::Instance().Log(TermTelemetry::Codes::OSCBG); commandIndex++; colorIndex++; } @@ -910,7 +810,6 @@ bool OutputStateMachineEngine::ActionOscDispatch(const wchar_t /*wch*/, { success = success && _dispatch->SetCursorColor(color); } - TermTelemetry::Instance().Log(TermTelemetry::Codes::OSCSCC); commandIndex++; colorIndex++; } @@ -926,13 +825,11 @@ bool OutputStateMachineEngine::ActionOscDispatch(const wchar_t /*wch*/, { success = _dispatch->SetClipboard(setClipboardContent); } - TermTelemetry::Instance().Log(TermTelemetry::Codes::OSCSCB); break; } case OscActionCodes::ResetCursorColor: { success = _dispatch->SetCursorColor(INVALID_COLOR); - TermTelemetry::Instance().Log(TermTelemetry::Codes::OSCRCC); break; } case OscActionCodes::Hyperlink: diff --git a/src/terminal/parser/OutputStateMachineEngine.hpp b/src/terminal/parser/OutputStateMachineEngine.hpp index a2aaee874e9..23a2d1508d5 100644 --- a/src/terminal/parser/OutputStateMachineEngine.hpp +++ b/src/terminal/parser/OutputStateMachineEngine.hpp @@ -13,7 +13,6 @@ Module Name: #include #include "../adapter/termDispatch.hpp" -#include "telemetry.hpp" #include "IStateMachineEngine.hpp" namespace Microsoft::Console::Render diff --git a/src/terminal/parser/ft_fuzzer/fuzzing_directed.h b/src/terminal/parser/ft_fuzzer/fuzzing_directed.h index 72dc0641cae..2ab6e1e23a5 100644 --- a/src/terminal/parser/ft_fuzzer/fuzzing_directed.h +++ b/src/terminal/parser/ft_fuzzer/fuzzing_directed.h @@ -337,6 +337,9 @@ namespace fuzz FuzzTraits m_traits{ TRAIT_DEFAULT }; }; + template + class CFuzzArraySize; + // The CFuzzArray class is designed to allow fuzzing of element // arrays, potentially reallocating a fuzzed version of the array // that is either larger or smaller than the template buffer. Whether @@ -353,8 +356,7 @@ namespace fuzz class CFuzzArray : public CFuzzBase { public: - template - friend class CFuzzArraySize; + friend class CFuzzArraySize<_Type1, _Type2, _Args...>; // Creates a CFuzzArray instance that wraps a buffer specified by // rg, together with its size (note that this is the number of elements @@ -643,8 +645,7 @@ namespace fuzz class CFuzzArraySize { public: - template - friend class CFuzzArray; + friend class CFuzzArray<__FUZZING_ALLOCATOR, _Type1, _Type2, _Args...>; CFuzzArraySize(__inout _Type2& cElems) : m_pcElems(&cElems), @@ -892,7 +893,7 @@ namespace fuzz __in ULONG cfte, __in _Type pt, __in _Args&&... args) : - CFuzzType(rgfte, cfte, pt, std::forward<_Args>(args)...) + CFuzzType<_Type, _Args...>(rgfte, cfte, pt, std::forward<_Args>(args)...) { } @@ -933,7 +934,7 @@ namespace fuzz __in ULONG cfte, __in _Type* psz, __in _Args... args) : - CFuzzType(rgfte, cfte, psz, std::forward<_Args>(args)...) + CFuzzType<_Type, _Args...>(rgfte, cfte, psz, std::forward<_Args>(args)...) { OnFuzzedValueFromMap(); } @@ -1061,7 +1062,7 @@ namespace fuzz __in ULONG cfte, __in _Type flags, __in _Args&&... args) : - CFuzzType(rgfte, cfte, flags, std::forward<_Args>(args)...) + CFuzzType<_Type, _Args...>(rgfte, cfte, flags, std::forward<_Args>(args)...) { } diff --git a/src/terminal/parser/lib/parser.vcxproj.filters b/src/terminal/parser/lib/parser.vcxproj.filters index 409f4e4071f..f62588b65d0 100644 --- a/src/terminal/parser/lib/parser.vcxproj.filters +++ b/src/terminal/parser/lib/parser.vcxproj.filters @@ -18,9 +18,6 @@ Source Files - - Source Files - Source Files @@ -38,9 +35,6 @@ Header Files - - Header Files - Header Files diff --git a/src/terminal/parser/parser-common.vcxitems b/src/terminal/parser/parser-common.vcxitems index f60ddec6eee..a929e694468 100644 --- a/src/terminal/parser/parser-common.vcxitems +++ b/src/terminal/parser/parser-common.vcxitems @@ -6,7 +6,6 @@ - Create @@ -19,7 +18,6 @@ - diff --git a/src/terminal/parser/precomp.h b/src/terminal/parser/precomp.h index 38a4422bc24..03611a47941 100644 --- a/src/terminal/parser/precomp.h +++ b/src/terminal/parser/precomp.h @@ -22,5 +22,4 @@ Module Name: #define ENABLE_INTSAFE_SIGNED_FUNCTIONS #include -#include "telemetry.hpp" #include "tracing.hpp" diff --git a/src/terminal/parser/sources.inc b/src/terminal/parser/sources.inc index 5e05cc566f7..e39b8a2aa3c 100644 --- a/src/terminal/parser/sources.inc +++ b/src/terminal/parser/sources.inc @@ -33,7 +33,6 @@ SOURCES = \ ..\stateMachine.cpp \ ..\InputStateMachineEngine.cpp \ ..\OutputStateMachineEngine.cpp \ - ..\telemetry.cpp \ ..\tracing.cpp \ ..\base64.cpp \ diff --git a/src/terminal/parser/stateMachine.cpp b/src/terminal/parser/stateMachine.cpp index d6a9aa31489..ea455f458d2 100644 --- a/src/terminal/parser/stateMachine.cpp +++ b/src/terminal/parser/stateMachine.cpp @@ -18,8 +18,7 @@ StateMachine::StateMachine(std::unique_ptr engine, const bo _parameters{}, _parameterLimitReached(false), _oscString{}, - _cachedSequence{ std::nullopt }, - _processingIndividually(false) + _cachedSequence{ std::nullopt } { _ActionClear(); } @@ -365,18 +364,6 @@ static constexpr bool _isApcIndicator(const wchar_t wch) noexcept return wch == L'_'; // 0x5F } -// Routine Description: -// - Determines if a character indicates an action that should be taken in the ground state - -// These are C0 characters and the C1 [single-character] CSI. -// Arguments: -// - wch - Character to check. -// Return Value: -// - True if it is. False if it isn't. -static constexpr bool _isActionableFromGround(const wchar_t wch) noexcept -{ - return (wch <= AsciiChars::US) || _isC1ControlCharacter(wch) || _isDelete(wch); -} - #pragma warning(pop) // Routine Description: @@ -447,7 +434,7 @@ void StateMachine::_ActionPrintString(const std::wstring_view string) void StateMachine::_ActionEscDispatch(const wchar_t wch) { _trace.TraceOnAction(L"EscDispatch"); - _trace.DispatchSequenceTrace(_SafeExecuteWithLog(wch, [=]() { + _trace.DispatchSequenceTrace(_SafeExecute([=]() { return _engine->ActionEscDispatch(_identifier.Finalize(wch)); })); } @@ -462,7 +449,7 @@ void StateMachine::_ActionEscDispatch(const wchar_t wch) void StateMachine::_ActionVt52EscDispatch(const wchar_t wch) { _trace.TraceOnAction(L"Vt52EscDispatch"); - _trace.DispatchSequenceTrace(_SafeExecuteWithLog(wch, [=]() { + _trace.DispatchSequenceTrace(_SafeExecute([=]() { return _engine->ActionVt52EscDispatch(_identifier.Finalize(wch), { _parameters.data(), _parameters.size() }); })); } @@ -477,7 +464,7 @@ void StateMachine::_ActionVt52EscDispatch(const wchar_t wch) void StateMachine::_ActionCsiDispatch(const wchar_t wch) { _trace.TraceOnAction(L"CsiDispatch"); - _trace.DispatchSequenceTrace(_SafeExecuteWithLog(wch, [=]() { + _trace.DispatchSequenceTrace(_SafeExecute([=]() { return _engine->ActionCsiDispatch(_identifier.Finalize(wch), { _parameters.data(), _parameters.size() }); })); } @@ -635,7 +622,7 @@ void StateMachine::_ActionOscPut(const wchar_t wch) void StateMachine::_ActionOscDispatch(const wchar_t wch) { _trace.TraceOnAction(L"OscDispatch"); - _trace.DispatchSequenceTrace(_SafeExecuteWithLog(wch, [=]() { + _trace.DispatchSequenceTrace(_SafeExecute([=]() { return _engine->ActionOscDispatch(wch, _oscParameter, _oscString); })); } @@ -650,7 +637,7 @@ void StateMachine::_ActionOscDispatch(const wchar_t wch) void StateMachine::_ActionSs3Dispatch(const wchar_t wch) { _trace.TraceOnAction(L"Ss3Dispatch"); - _trace.DispatchSequenceTrace(_SafeExecuteWithLog(wch, [=]() { + _trace.DispatchSequenceTrace(_SafeExecute([=]() { return _engine->ActionSs3Dispatch(wch, { _parameters.data(), _parameters.size() }); })); } @@ -666,7 +653,7 @@ void StateMachine::_ActionDcsDispatch(const wchar_t wch) { _trace.TraceOnAction(L"DcsDispatch"); - const auto success = _SafeExecuteWithLog(wch, [=]() { + const auto success = _SafeExecute([=]() { _dcsStringHandler = _engine->ActionDcsDispatch(_identifier.Finalize(wch), { _parameters.data(), _parameters.size() }); // If the returned handler is null, the sequence is not supported. return _dcsStringHandler != nullptr; @@ -1821,6 +1808,119 @@ bool StateMachine::FlushToTerminal() return success; } +// Disable vectorization-unfriendly warnings. +#pragma warning(push) +#pragma warning(disable : 26429) // Symbol '...' is never tested for nullness, it can be marked as not_null (f.23). +#pragma warning(disable : 26472) // Don't use a static_cast for arithmetic conversions. Use brace initialization, gsl::narrow_cast or gsl::narrow (type.1). +#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). + +// Returns true for C0 characters and C1 [single-character] CSI. +constexpr bool isActionableFromGround(const wchar_t wch) noexcept +{ + // This is equivalent to: + // return (wch <= 0x1f) || (wch >= 0x7f && wch <= 0x9f); + // It's written like this to get MSVC to emit optimal assembly for findActionableFromGround. + // It lacks the ability to turn boolean operators into binary operations and also happens + // to fail to optimize the printable-ASCII range check into a subtraction & comparison. + return (wch <= 0x1f) | (static_cast(wch - 0x7f) <= 0x20); +} + +[[msvc::forceinline]] static size_t findActionableFromGroundPlain(const wchar_t* beg, const wchar_t* end, const wchar_t* it) noexcept +{ +#pragma loop(no_vector) + for (; it < end && !isActionableFromGround(*it); ++it) + { + } + return it - beg; +} + +static size_t findActionableFromGround(const wchar_t* data, size_t count) noexcept +{ + // The following vectorized code replicates isActionableFromGround which is equivalent to: + // (wch <= 0x1f) || (wch >= 0x7f && wch <= 0x9f) + // or rather its more machine friendly equivalent: + // (wch <= 0x1f) | ((wch - 0x7f) <= 0x20) +#if defined(TIL_SSE_INTRINSICS) + + auto it = data; + + for (const auto end = data + (count & ~size_t{ 7 }); it < end; it += 8) + { + const auto wch = _mm_loadu_si128(reinterpret_cast(it)); + const auto z = _mm_setzero_si128(); + + // Dealing with unsigned numbers in SSE2 is annoying because it has poor support for that. + // We'll use subtractions with saturation ("SubS") to work around that. A check like + // a < b can be implemented as "max(0, a - b) == 0" and "max(0, a - b)" is what "SubS" is. + + // Check for (wch < 0x20) + auto a = _mm_subs_epu16(wch, _mm_set1_epi16(0x1f)); + // Check for "((wch - 0x7f) <= 0x20)" by adding 0x10000-0x7f, which overflows to a + // negative number if "wch >= 0x7f" and then subtracting 0x9f-0x7f with saturation to an + // unsigned number (= can't go lower than 0), which results in all numbers up to 0x9f to be 0. + auto b = _mm_subs_epu16(_mm_add_epi16(wch, _mm_set1_epi16(static_cast(0xff81))), _mm_set1_epi16(0x20)); + a = _mm_cmpeq_epi16(a, z); + b = _mm_cmpeq_epi16(b, z); + + const auto c = _mm_or_si128(a, b); + const auto mask = _mm_movemask_epi8(c); + + if (mask) + { + unsigned long offset; + _BitScanForward(&offset, mask); + it += offset / 2; + return it - data; + } + } + + return findActionableFromGroundPlain(data, data + count, it); + +#elif defined(TIL_ARM_NEON_INTRINSICS) + + auto it = data; + uint64_t mask; + + for (const auto end = data + (count & ~size_t{ 7 }); it < end;) + { + const auto wch = vld1q_u16(it); + const auto a = vcleq_u16(wch, vdupq_n_u16(0x1f)); + const auto b = vcleq_u16(vsubq_u16(wch, vdupq_n_u16(0x7f)), vdupq_n_u16(0x20)); + const auto c = vorrq_u16(a, b); + + mask = vgetq_lane_u64(c, 0); + if (mask) + { + goto exitWithMask; + } + it += 4; + + mask = vgetq_lane_u64(c, 1); + if (mask) + { + goto exitWithMask; + } + it += 4; + } + + return findActionableFromGroundPlain(data, data + count, it); + +exitWithMask: + unsigned long offset; + _BitScanForward64(&offset, mask); + it += offset / 16; + return it - data; + +#else + + return findActionableFromGroundPlain(data, data + count, p); + +#endif +} + +#pragma warning(pop) + // Routine Description: // - Helper for entry to the state machine. Will take an array of characters // and print as many as it can without encountering a character indicating @@ -1832,79 +1932,54 @@ bool StateMachine::FlushToTerminal() // - void StateMachine::ProcessString(const std::wstring_view string) { - size_t start = 0; - auto current = start; - + size_t i = 0; _currentString = string; _runOffset = 0; _runSize = 0; - while (current < string.size()) + if (_state != VTStates::Ground) { - // The run will be everything from the start INCLUDING the current one - // in case we process the current character and it turns into a passthrough - // fallback that picks up this _run inside `FlushToTerminal` above. - _runOffset = start; - _runSize = current - start + 1; + // Jump straight to where we need to. +#pragma warning(suppress : 26438) // Avoid 'goto'(es .76). + goto processStringLoopVtStart; + } - if (_processingIndividually) + while (i < string.size()) + { { - // Note whether we're dealing with the last character in the buffer. - _processingLastCharacter = (current + 1 >= string.size()); - // If we're processing characters individually, send it to the state machine. - ProcessCharacter(til::at(string, current)); - ++current; - if (_state == VTStates::Ground) // Then check if we're back at ground. If we are, the next character (pwchCurr) - { // is the start of the next run of characters that might be printable. - _processingIndividually = false; - start = current; + _runOffset = i; + // Pointer arithmetic is perfectly fine for our hot path. +#pragma warning(suppress : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1).) + _runSize = findActionableFromGround(string.data() + i, string.size() - i); + + if (_runSize) + { + _ActionPrintString(_CurrentRun()); + + i += _runSize; + _runOffset = i; + _runSize = 0; } } - else + + processStringLoopVtStart: + if (i >= string.size()) { - if (_isActionableFromGround(til::at(string, current))) // If the current char is the start of an escape sequence, or should be executed in ground state... - { - if (_runSize > 0) - { - // Because the run above is composed INCLUDING current, we must - // trim it off here since we just determined it's actionable - // and only pass through everything before it. - _runSize -= 1; - _ActionPrintString(_CurrentRun()); // ... print all the chars leading up to it as part of the run... - } - - _processingIndividually = true; // begin processing future characters individually... - start = current; - continue; - } - else - { - ++current; // Otherwise, add this char to the current run to be printed. - } + break; } - } - // When we leave the loop, current has been advanced to the length of the string itself - // (or one past the array index to the final char) so this `substr` operation doesn't +1 - // to include the final character (unlike the one inside the top of the loop above.) - if (start < string.size()) - { - _runOffset = start; - _runSize = std::string::npos; - } - else - { - _runSize = 0; + do + { + _runSize++; + _processingLastCharacter = i + 1 >= string.size(); + // If we're processing characters individually, send it to the state machine. + ProcessCharacter(til::at(string, i)); + ++i; + } while (i < string.size() && _state != VTStates::Ground); } - const auto run = _CurrentRun(); // If we're at the end of the string and have remaining un-printed characters, - if (!_processingIndividually && !run.empty()) - { - // print the rest of the characters in the string - _ActionPrintString(run); - } - else if (_processingIndividually) + if (_state != VTStates::Ground) { // One of the "weird things" in VT input is the case of something like // alt+[. In VT, that's encoded as `\x1b[`. However, that's @@ -1924,6 +1999,7 @@ void StateMachine::ProcessString(const std::wstring_view string) // last character of the string. For our previous `\x1b[` scenario, that // means we'll make sure to call `_ActionEscDispatch('[')`., which will // properly decode the string as alt+[. + const auto run = _CurrentRun(); if (_isEngineForInput) { @@ -2063,17 +2139,6 @@ catch (...) return false; } -template -bool StateMachine::_SafeExecuteWithLog(const wchar_t wch, TLambda&& lambda) -{ - const bool success = _SafeExecute(std::forward(lambda)); - if (!success) - { - TermTelemetry::Instance().LogFailed(wch); - } - return success; -} - void StateMachine::_ExecuteCsiCompleteCallback() { if (_onCsiCompleteCallback) diff --git a/src/terminal/parser/stateMachine.hpp b/src/terminal/parser/stateMachine.hpp index a51351e456f..4023299c0c6 100644 --- a/src/terminal/parser/stateMachine.hpp +++ b/src/terminal/parser/stateMachine.hpp @@ -15,7 +15,6 @@ Module Name: #pragma once #include "IStateMachineEngine.hpp" -#include "telemetry.hpp" #include "tracing.hpp" #include @@ -141,8 +140,6 @@ namespace Microsoft::Console::VirtualTerminal template bool _SafeExecute(TLambda&& lambda); - template - bool _SafeExecuteWithLog(const wchar_t wch, TLambda&& lambda); void _ExecuteCsiCompleteCallback(); @@ -204,7 +201,6 @@ namespace Microsoft::Console::VirtualTerminal // This is tracked per state machine instance so that separate calls to Process* // can start and finish a sequence. - bool _processingIndividually; bool _processingLastCharacter; std::function _onCsiCompleteCallback; diff --git a/src/terminal/parser/telemetry.cpp b/src/terminal/parser/telemetry.cpp deleted file mode 100644 index 98d752a3a45..00000000000 --- a/src/terminal/parser/telemetry.cpp +++ /dev/null @@ -1,311 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" - -#include "telemetry.hpp" - -#pragma warning(push) -#pragma warning(disable : 26494) // _Tlgdata uninitialized from TraceLoggingWrite -#pragma warning(disable : 26477) // Use nullptr instead of NULL or 0 from TraceLoggingWrite -#pragma warning(disable : 26485) // _Tlgdata, no array to pointer decay from TraceLoggingWrite -#pragma warning(disable : 26446) // Prefer gsl::at over unchecked subscript from TraceLoggingLevel -#pragma warning(disable : 26482) // Only index to arrays with constant expressions from TraceLoggingLevel - -TRACELOGGING_DEFINE_PROVIDER(g_hConsoleVirtTermParserEventTraceProvider, - "Microsoft.Windows.Console.VirtualTerminal.Parser", - // {c9ba2a84-d3ca-5e19-2bd6-776a0910cb9d} - (0xc9ba2a84, 0xd3ca, 0x5e19, 0x2b, 0xd6, 0x77, 0x6a, 0x09, 0x10, 0xcb, 0x9d)); - -using namespace Microsoft::Console::VirtualTerminal; - -#pragma warning(push) -// Disable 4351 so we can initialize the arrays to 0 without a warning. -#pragma warning(disable : 4351) -TermTelemetry::TermTelemetry() noexcept : - _uiTimesUsedCurrent(0), - _uiTimesFailedCurrent(0), - _uiTimesFailedOutsideRangeCurrent(0), - _uiTimesUsed(), - _uiTimesFailed(), - _uiTimesFailedOutsideRange(0), - _activityId(), - _fShouldWriteFinalLog(false) -{ - TraceLoggingRegister(g_hConsoleVirtTermParserEventTraceProvider); - - // Create a random activityId just in case it doesn't get set later in SetActivityId(). - EventActivityIdControl(EVENT_ACTIVITY_CTRL_CREATE_ID, &_activityId); -} -#pragma warning(pop) - -TermTelemetry::~TermTelemetry() -{ - try - { - WriteFinalTraceLog(); - TraceLoggingUnregister(g_hConsoleVirtTermParserEventTraceProvider); - } - CATCH_LOG() -} - -// Routine Description: -// - Logs the usage of a particular VT100 code. -// -// Arguments: -// - code - VT100 code. -// Return Value: -// - -void TermTelemetry::Log(const Codes code) noexcept -{ - // Initially we wanted to pass over a string (ex. "CUU") and use a dictionary data type to hold the counts. - // However we would have to search through the dictionary every time we called this method, so we decided - // to use an array which has very quick access times. - // The downside is we have to create an enum type, and then convert them to strings when we finally - // send out the telemetry, but the upside is we should have very good performance. - _uiTimesUsed[code]++; - _uiTimesUsedCurrent++; -} - -// Routine Description: -// - Logs a particular VT100 escape code failed or was unsupported. -// -// Arguments: -// - code - VT100 code. -// Return Value: -// - -void TermTelemetry::LogFailed(const wchar_t wch) noexcept -{ - if (wch > CHAR_MAX) - { - _uiTimesFailedOutsideRange++; - _uiTimesFailedOutsideRangeCurrent++; - } - else - { - // Even though we pass over a wide character, we only care about the ASCII single byte character. - _uiTimesFailed[wch]++; - _uiTimesFailedCurrent++; - } -} - -// Routine Description: -// - Gets and resets the total count of codes used. -// -// Arguments: -// - -// Return Value: -// - total number. -unsigned int TermTelemetry::GetAndResetTimesUsedCurrent() noexcept -{ - const auto temp = _uiTimesUsedCurrent; - _uiTimesUsedCurrent = 0; - return temp; -} - -// Routine Description: -// - Gets and resets the total count of codes failed. -// -// Arguments: -// - -// Return Value: -// - total number. -unsigned int TermTelemetry::GetAndResetTimesFailedCurrent() noexcept -{ - const auto temp = _uiTimesFailedCurrent; - _uiTimesFailedCurrent = 0; - return temp; -} - -// Routine Description: -// - Gets and resets the total count of codes failed outside the valid range. -// -// Arguments: -// - -// Return Value: -// - total number. -unsigned int TermTelemetry::GetAndResetTimesFailedOutsideRangeCurrent() noexcept -{ - const auto temp = _uiTimesFailedOutsideRangeCurrent; - _uiTimesFailedOutsideRangeCurrent = 0; - return temp; -} - -// Routine Description: -// - Lets us know whether we should write the final log. Typically set true when the console has been -// interacted with, to help reduce the amount of telemetry we're sending. -// -// Arguments: -// - writeLog - true if we should write the log. -// Return Value: -// - -void TermTelemetry::SetShouldWriteFinalLog(const bool writeLog) noexcept -{ - _fShouldWriteFinalLog = writeLog; -} - -// Routine Description: -// - Sets the activity Id, so we can match our events with other providers (such as Microsoft.Windows.Console.Host). -// -// Arguments: -// - activityId - Pointer to Guid to set our activity Id to. -// Return Value: -// - -void TermTelemetry::SetActivityId(const GUID* activityId) noexcept -{ - _activityId = *activityId; -} - -// Routine Description: -// - Writes the final log of all the telemetry collected. The primary reason to send back a final log instead -// of individual events is to reduce the amount of telemetry being sent and potentially overloading our servers. -// -// Arguments: -// - code - VT100 code. -// Return Value: -// - -void TermTelemetry::WriteFinalTraceLog() const -{ - if (_fShouldWriteFinalLog) - { - // Determine if we've logged any VT100 sequences at all. - auto fLoggedSequence = (_uiTimesFailedOutsideRange > 0); - - if (!fLoggedSequence) - { - for (auto n = 0; n < ARRAYSIZE(_uiTimesUsed); n++) - { - if (_uiTimesUsed[n] > 0) - { - fLoggedSequence = true; - break; - } - } - } - - if (!fLoggedSequence) - { - for (auto n = 0; n < ARRAYSIZE(_uiTimesFailed); n++) - { - if (_uiTimesFailed[n] > 0) - { - fLoggedSequence = true; - break; - } - } - } - - // Only send telemetry if we've logged some VT100 sequences. This should help reduce the amount of unnecessary - // telemetry being sent. - if (fLoggedSequence) - { - // I could use the TraceLoggingUIntArray, but then we would have to know the order of the enums on the backend. - // So just log each enum count separately with its string representation which makes it more human readable. - // Set the related activity to NULL since we aren't using it. - TraceLoggingWriteActivity(g_hConsoleVirtTermParserEventTraceProvider, - "ControlCodesUsed", - &_activityId, - NULL, - TraceLoggingUInt32(_uiTimesUsed[CUU], "CUU"), - TraceLoggingUInt32(_uiTimesUsed[CUD], "CUD"), - TraceLoggingUInt32(_uiTimesUsed[CUF], "CUF"), - TraceLoggingUInt32(_uiTimesUsed[CUB], "CUB"), - TraceLoggingUInt32(_uiTimesUsed[CNL], "CNL"), - TraceLoggingUInt32(_uiTimesUsed[CPL], "CPL"), - TraceLoggingUInt32(_uiTimesUsed[CHA], "CHA"), - TraceLoggingUInt32(_uiTimesUsed[CUP], "CUP"), - TraceLoggingUInt32(_uiTimesUsed[ED], "ED"), - TraceLoggingUInt32(_uiTimesUsed[DECSED], "DECSED"), - TraceLoggingUInt32(_uiTimesUsed[EL], "EL"), - TraceLoggingUInt32(_uiTimesUsed[DECSEL], "DECSEL"), - TraceLoggingUInt32(_uiTimesUsed[SGR], "SGR"), - TraceLoggingUInt32(_uiTimesUsed[DECBI], "DECBI"), - TraceLoggingUInt32(_uiTimesUsed[DECSC], "DECSC"), - TraceLoggingUInt32(_uiTimesUsed[DECRC], "DECRC"), - TraceLoggingUInt32(_uiTimesUsed[DECFI], "DECFI"), - TraceLoggingUInt32(_uiTimesUsed[SM], "SM"), - TraceLoggingUInt32(_uiTimesUsed[DECSET], "DECSET"), - TraceLoggingUInt32(_uiTimesUsed[RM], "RM"), - TraceLoggingUInt32(_uiTimesUsed[DECRST], "DECRST"), - TraceLoggingUInt32(_uiTimesUsed[DECKPAM], "DECKPAM"), - TraceLoggingUInt32(_uiTimesUsed[DECKPNM], "DECKPNM"), - TraceLoggingUInt32(_uiTimesUsed[DSR], "DSR"), - TraceLoggingUInt32(_uiTimesUsed[DA], "DA"), - TraceLoggingUInt32(_uiTimesUsed[DA2], "DA2"), - TraceLoggingUInt32(_uiTimesUsed[DA3], "DA3"), - TraceLoggingUInt32(_uiTimesUsed[DECREQTPARM], "DECREQTPARM"), - TraceLoggingUInt32(_uiTimesUsed[VPA], "VPA"), - TraceLoggingUInt32(_uiTimesUsed[HPR], "HPR"), - TraceLoggingUInt32(_uiTimesUsed[VPR], "VPR"), - TraceLoggingUInt32(_uiTimesUsed[ICH], "ICH"), - TraceLoggingUInt32(_uiTimesUsed[DCH], "DCH"), - TraceLoggingUInt32(_uiTimesUsed[IL], "IL"), - TraceLoggingUInt32(_uiTimesUsed[DL], "DL"), - TraceLoggingUInt32(_uiTimesUsed[SU], "SU"), - TraceLoggingUInt32(_uiTimesUsed[SD], "SD"), - TraceLoggingUInt32(_uiTimesUsed[ANSISYSRC], "ANSISYSRC"), - TraceLoggingUInt32(_uiTimesUsed[DECSTBM], "DECSTBM"), - TraceLoggingUInt32(_uiTimesUsed[DECSLRM], "DECSLRM"), - TraceLoggingUInt32(_uiTimesUsed[NEL], "NEL"), - TraceLoggingUInt32(_uiTimesUsed[IND], "IND"), - TraceLoggingUInt32(_uiTimesUsed[RI], "RI"), - TraceLoggingUInt32(_uiTimesUsed[OSCWT], "OscWindowTitle"), - TraceLoggingUInt32(_uiTimesUsed[HTS], "HTS"), - TraceLoggingUInt32(_uiTimesUsed[CHT], "CHT"), - TraceLoggingUInt32(_uiTimesUsed[CBT], "CBT"), - TraceLoggingUInt32(_uiTimesUsed[TBC], "TBC"), - TraceLoggingUInt32(_uiTimesUsed[ECH], "ECH"), - TraceLoggingUInt32(_uiTimesUsed[DesignateG0], "DesignateG0"), - TraceLoggingUInt32(_uiTimesUsed[DesignateG1], "DesignateG1"), - TraceLoggingUInt32(_uiTimesUsed[DesignateG2], "DesignateG2"), - TraceLoggingUInt32(_uiTimesUsed[DesignateG3], "DesignateG3"), - TraceLoggingUInt32(_uiTimesUsed[LS2], "LS2"), - TraceLoggingUInt32(_uiTimesUsed[LS3], "LS3"), - TraceLoggingUInt32(_uiTimesUsed[LS1R], "LS1R"), - TraceLoggingUInt32(_uiTimesUsed[LS2R], "LS2R"), - TraceLoggingUInt32(_uiTimesUsed[LS3R], "LS3R"), - TraceLoggingUInt32(_uiTimesUsed[SS2], "SS2"), - TraceLoggingUInt32(_uiTimesUsed[SS3], "SS3"), - TraceLoggingUInt32(_uiTimesUsed[DOCS], "DOCS"), - TraceLoggingUInt32(_uiTimesUsed[HVP], "HVP"), - TraceLoggingUInt32(_uiTimesUsed[DECSTR], "DECSTR"), - TraceLoggingUInt32(_uiTimesUsed[RIS], "RIS"), - TraceLoggingUInt32(_uiTimesUsed[DECSCUSR], "DECSCUSR"), - TraceLoggingUInt32(_uiTimesUsed[DECSCA], "DECSCA"), - TraceLoggingUInt32(_uiTimesUsed[DTTERM_WM], "DTTERM_WM"), - TraceLoggingUInt32(_uiTimesUsed[OSCCT], "OscColorTable"), - TraceLoggingUInt32(_uiTimesUsed[OSCSCC], "OscSetCursorColor"), - TraceLoggingUInt32(_uiTimesUsed[OSCRCC], "OscResetCursorColor"), - TraceLoggingUInt32(_uiTimesUsed[OSCFG], "OscForegroundColor"), - TraceLoggingUInt32(_uiTimesUsed[OSCBG], "OscBackgroundColor"), - TraceLoggingUInt32(_uiTimesUsed[OSCSCB], "OscSetClipboard"), - TraceLoggingUInt32(_uiTimesUsed[REP], "REP"), - TraceLoggingUInt32(_uiTimesUsed[DECAC1], "DECAC1"), - TraceLoggingUInt32(_uiTimesUsed[DECSWL], "DECSWL"), - TraceLoggingUInt32(_uiTimesUsed[DECDWL], "DECDWL"), - TraceLoggingUInt32(_uiTimesUsed[DECDHL], "DECDHL"), - TraceLoggingUInt32(_uiTimesUsed[DECALN], "DECALN"), - TraceLoggingUInt32(_uiTimesUsed[XTPUSHSGR], "XTPUSHSGR"), - TraceLoggingUInt32(_uiTimesUsed[XTPOPSGR], "XTPOPSGR"), - TraceLoggingUInt32(_uiTimesUsed[DECRQM], "DECRQM"), - TraceLoggingUInt32(_uiTimesUsed[DECCARA], "DECCARA"), - TraceLoggingUInt32(_uiTimesUsed[DECRARA], "DECRARA"), - TraceLoggingUInt32(_uiTimesUsed[DECCRA], "DECCRA"), - TraceLoggingUInt32(_uiTimesUsed[DECRQPSR], "DECRQPSR"), - TraceLoggingUInt32(_uiTimesUsed[DECFRA], "DECFRA"), - TraceLoggingUInt32(_uiTimesUsed[DECERA], "DECERA"), - TraceLoggingUInt32(_uiTimesUsed[DECSERA], "DECSERA"), - TraceLoggingUInt32(_uiTimesUsed[DECIC], "DECIC"), - TraceLoggingUInt32(_uiTimesUsed[DECDC], "DECDC"), - TraceLoggingUInt32(_uiTimesUsed[DECSACE], "DECSACE"), - TraceLoggingUInt32(_uiTimesUsed[DECRQCRA], "DECRQCRA"), - TraceLoggingUInt32(_uiTimesUsed[DECINVM], "DECINVM"), - TraceLoggingUInt32(_uiTimesUsed[DECAC], "DECAC"), - TraceLoggingUInt32(_uiTimesUsed[DECPS], "DECPS"), - TraceLoggingUInt32Array(_uiTimesFailed, ARRAYSIZE(_uiTimesFailed), "Failed"), - TraceLoggingUInt32(_uiTimesFailedOutsideRange, "FailedOutsideRange")); - } - } -} - -#pragma warning(pop) diff --git a/src/terminal/parser/telemetry.hpp b/src/terminal/parser/telemetry.hpp deleted file mode 100644 index dd71c61a104..00000000000 --- a/src/terminal/parser/telemetry.hpp +++ /dev/null @@ -1,164 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -/* -Module Name: -- telemetry.hpp - -Abstract: -- This module is used for recording all telemetry feedback from the console virtual terminal parser -*/ -#pragma once - -// Including TraceLogging essentials for the binary -#include -#include -#include -#include "climits" - -TRACELOGGING_DECLARE_PROVIDER(g_hConsoleVirtTermParserEventTraceProvider); - -namespace Microsoft::Console::VirtualTerminal -{ - class TermTelemetry sealed - { - public: - // Implement this as a singleton class. - static TermTelemetry& Instance() noexcept - { - static TermTelemetry s_Instance; - return s_Instance; - } - - // Names primarily from http://inwap.com/pdp10/ansicode.txt - enum Codes - { - CUU = 0, - CUD, - CUF, - CUB, - CNL, - CPL, - CHA, - CUP, - ED, - DECSED, - EL, - DECSEL, - SGR, - DECBI, - DECSC, - DECRC, - DECFI, - SM, - DECSET, - RM, - DECRST, - DECKPAM, - DECKPNM, - DSR, - DA, - DA2, - DA3, - DECREQTPARM, - VPA, - HPR, - VPR, - ICH, - DCH, - SU, - SD, - ANSISYSRC, - IL, - DL, - DECSTBM, - DECSLRM, - NEL, - IND, - RI, - OSCWT, - HTS, - CHT, - CBT, - TBC, - ECH, - DesignateG0, - DesignateG1, - DesignateG2, - DesignateG3, - LS2, - LS3, - LS1R, - LS2R, - LS3R, - SS2, - SS3, - DOCS, - HVP, - DECSTR, - RIS, - DECSCUSR, - DECSCA, - DTTERM_WM, - OSCCT, - OSCSCC, - OSCRCC, - REP, - OSCFG, - OSCBG, - DECAC1, - DECSWL, - DECDWL, - DECDHL, - DECALN, - OSCSCB, - XTPUSHSGR, - XTPOPSGR, - DECRQM, - DECCARA, - DECRARA, - DECCRA, - DECRQPSR, - DECFRA, - DECERA, - DECSERA, - DECIC, - DECDC, - DECSACE, - DECRQCRA, - DECINVM, - DECAC, - DECPS, - // Only use this last enum as a count of the number of codes. - NUMBER_OF_CODES - }; - void Log(const Codes code) noexcept; - void LogFailed(const wchar_t wch) noexcept; - void SetShouldWriteFinalLog(const bool writeLog) noexcept; - void SetActivityId(const GUID* activityId) noexcept; - unsigned int GetAndResetTimesUsedCurrent() noexcept; - unsigned int GetAndResetTimesFailedCurrent() noexcept; - unsigned int GetAndResetTimesFailedOutsideRangeCurrent() noexcept; - - private: - // Used to prevent multiple instances - TermTelemetry() noexcept; - ~TermTelemetry(); - TermTelemetry(const TermTelemetry&) = delete; - TermTelemetry(TermTelemetry&&) = delete; - TermTelemetry& operator=(const TermTelemetry&) = delete; - TermTelemetry& operator=(TermTelemetry&&) = delete; - - void WriteFinalTraceLog() const; - - unsigned int _uiTimesUsedCurrent; - unsigned int _uiTimesFailedCurrent; - unsigned int _uiTimesFailedOutsideRangeCurrent; - unsigned int _uiTimesUsed[NUMBER_OF_CODES]; - unsigned int _uiTimesFailed[CHAR_MAX + 1]; - unsigned int _uiTimesFailedOutsideRange; - GUID _activityId; - - bool _fShouldWriteFinalLog; - }; -} diff --git a/src/terminal/parser/tracing.cpp b/src/terminal/parser/tracing.cpp index 5ddc5f453e3..9ab684ac95d 100644 --- a/src/terminal/parser/tracing.cpp +++ b/src/terminal/parser/tracing.cpp @@ -10,6 +10,11 @@ using namespace Microsoft::Console::VirtualTerminal; #pragma warning(disable : 26447) // The function is declared 'noexcept' but calls function '_tlgWrapBinary()' which may throw exceptions #pragma warning(disable : 26477) // Use 'nullptr' rather than 0 or NULL +TRACELOGGING_DEFINE_PROVIDER(g_hConsoleVirtTermParserEventTraceProvider, + "Microsoft.Windows.Console.VirtualTerminal.Parser", + // {c9ba2a84-d3ca-5e19-2bd6-776a0910cb9d} + (0xc9ba2a84, 0xd3ca, 0x5e19, 0x2b, 0xd6, 0x77, 0x6a, 0x09, 0x10, 0xcb, 0x9d)); + void ParserTracing::TraceStateChange(_In_z_ const wchar_t* name) const noexcept { TraceLoggingWrite(g_hConsoleVirtTermParserEventTraceProvider, diff --git a/src/terminal/parser/tracing.hpp b/src/terminal/parser/tracing.hpp index 27e3e3352d6..ecc357c3236 100644 --- a/src/terminal/parser/tracing.hpp +++ b/src/terminal/parser/tracing.hpp @@ -14,7 +14,11 @@ Module Name: #pragma once -#include "telemetry.hpp" +// Including TraceLogging essentials for the binary +#include +#include + +TRACELOGGING_DECLARE_PROVIDER(g_hConsoleVirtTermParserEventTraceProvider); namespace Microsoft::Console::VirtualTerminal { diff --git a/src/terminal/parser/ut_parser/InputEngineTest.cpp b/src/terminal/parser/ut_parser/InputEngineTest.cpp index c7c090da022..5ae36992574 100644 --- a/src/terminal/parser/ut_parser/InputEngineTest.cpp +++ b/src/terminal/parser/ut_parser/InputEngineTest.cpp @@ -960,27 +960,8 @@ void InputEngineTest::AltIntermediateTest() // We'll test this by creating both a TerminalInput and an // InputStateMachine, and piping the KeyEvents generated by the // InputStateMachine into the TerminalInput. - std::wstring expectedTranslation{}; - - // First create the callback TerminalInput will call - this will be - // triggered second, after both the state machine and the TerminalInput have - // translated the characters. - auto pfnTerminalInputCallback = [&](std::deque>& inEvents) { - // Get all the characters: - std::wstring wstr = L""; - for (auto& ev : inEvents) - { - if (ev->EventType() == InputEventType::KeyEvent) - { - auto& k = static_cast(*ev); - auto wch = k.GetCharData(); - wstr += wch; - } - } - - VERIFY_ARE_EQUAL(expectedTranslation, wstr); - }; - TerminalInput terminalInput{ pfnTerminalInputCallback }; + std::wstring translation; + TerminalInput terminalInput; // Create the callback that's fired when the state machine wants to write // input. We'll take the events and put them straight into the @@ -988,7 +969,10 @@ void InputEngineTest::AltIntermediateTest() auto pfnInputStateMachineCallback = [&](std::deque>& inEvents) { for (auto& ev : inEvents) { - terminalInput.HandleKey(ev.get()); + if (const auto out = terminalInput.HandleKey(ev.get())) + { + translation.append(*out); + } } }; auto dispatch = std::make_unique(pfnInputStateMachineCallback, &testState); @@ -1001,14 +985,16 @@ void InputEngineTest::AltIntermediateTest() // run it through the terminalInput translator. We should get ^[/^E back // out. std::wstring seq = L"\x1b/"; - expectedTranslation = seq; + translation.clear(); Log::Comment(NoThrowString().Format(L"Processing \"\\x1b/\"")); stateMachine->ProcessString(seq); + VERIFY_ARE_EQUAL(seq, translation); seq = L"\x05"; // 0x05 is ^E - expectedTranslation = seq; + translation.clear(); Log::Comment(NoThrowString().Format(L"Processing \"\\x05\"")); stateMachine->ProcessString(seq); + VERIFY_ARE_EQUAL(seq, translation); VerifyExpectedInputDrained(); } diff --git a/src/til/ut_til/BitmapTests.cpp b/src/til/ut_til/BitmapTests.cpp index 39847cafdf4..ee093f52a79 100644 --- a/src/til/ut_til/BitmapTests.cpp +++ b/src/til/ut_til/BitmapTests.cpp @@ -632,26 +632,19 @@ class BitmapTests _checkBits(expectedSet, bitmap); } - TEST_METHOD(SetResetExceptions) + TEST_METHOD(SetResetOutOfBounds) { til::bitmap map{ til::size{ 4, 4 } }; Log::Comment(L"1.) SetPoint out of bounds."); - { - auto fn = [&]() { - map.set(til::point{ 10, 10 }); - }; - - VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_INVALIDARG; }); - } + map.set(til::point{ 10, 10 }); Log::Comment(L"2.) SetRectangle out of bounds."); - { - auto fn = [&]() { - map.set(til::rect{ til::point{ 2, 2 }, til::size{ 10, 10 } }); - }; + map.set(til::rect{ til::point{ 2, 2 }, til::size{ 10, 10 } }); - VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_INVALIDARG; }); - } + const auto runs = map.runs(); + VERIFY_ARE_EQUAL(2u, runs.size()); + VERIFY_ARE_EQUAL(til::rect(2, 2, 4, 3), runs[0]); + VERIFY_ARE_EQUAL(til::rect(2, 3, 4, 4), runs[1]); } TEST_METHOD(Resize) diff --git a/src/til/ut_til/SmallVectorTests.cpp b/src/til/ut_til/SmallVectorTests.cpp index 9eaab3c0c65..f1a2ed88e54 100644 --- a/src/til/ut_til/SmallVectorTests.cpp +++ b/src/til/ut_til/SmallVectorTests.cpp @@ -368,4 +368,24 @@ class SmallVectorTests til::small_vector expected{ { -1, -1, 0, 1, 2, 3, 3, 3, 4, 5, 5 } }; VERIFY_ARE_EQUAL(expected, actual); } + + TEST_METHOD(CopyOntoItself) + { + til::small_vector actual(3); + actual.operator=(actual); + + const til::small_vector expected(3); + VERIFY_ARE_EQUAL(expected, actual); + } + + TEST_METHOD(MoveOntoItself) + { + til::small_vector actual; + actual.resize(3); + actual.operator=(std::move(actual)); + + til::small_vector expected; + expected.resize(3); + VERIFY_ARE_EQUAL(expected, actual); + } }; diff --git a/src/tools/RenderingTests/RenderingTests.vcxproj b/src/tools/RenderingTests/RenderingTests.vcxproj index 0824d3dd669..5de14c0f43e 100644 --- a/src/tools/RenderingTests/RenderingTests.vcxproj +++ b/src/tools/RenderingTests/RenderingTests.vcxproj @@ -4,6 +4,7 @@ 16.0 Win32Proj {37c995e0-2349-4154-8e77-4a52c0c7f46d} + RenderingTests RenderingTests 10.0 diff --git a/src/tools/benchcat/benchcat.vcxproj b/src/tools/benchcat/benchcat.vcxproj new file mode 100644 index 00000000000..67a51675a14 --- /dev/null +++ b/src/tools/benchcat/benchcat.vcxproj @@ -0,0 +1,45 @@ + + + + 16.0 + Win32Proj + {2C836962-9543-4CE5-B834-D28E1F124B66} + benchcat + benchcat + 10.0 + bc + + + + + NotUsing + + + Console + + + + false + false + + + + false + false + false + NODEFAULTLIB;%(PreprocessorDefinitions) + false + + + main + true + + + + + + + + + + diff --git a/src/tools/benchcat/crt.cpp b/src/tools/benchcat/crt.cpp new file mode 100644 index 00000000000..f3dc3f6016e --- /dev/null +++ b/src/tools/benchcat/crt.cpp @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#ifdef NODEFAULTLIB + +#include + +#pragma function(memcpy) +void* memcpy(void* dst, const void* src, size_t size) +{ + __movsb(static_cast(dst), static_cast(src), size); + return dst; +} + +#pragma function(memset) +void* memset(void* dst, int val, size_t size) +{ + __stosb(static_cast(dst), static_cast(val), size); + return dst; +} + +#endif diff --git a/src/tools/benchcat/main.cpp b/src/tools/benchcat/main.cpp new file mode 100644 index 00000000000..ff2e63434be --- /dev/null +++ b/src/tools/benchcat/main.cpp @@ -0,0 +1,692 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#define NOMINMAX +#define WIN32_LEAN_AND_MEAN + +#include +#include +#include +#include + +#include + +#include "crt.cpp" + +// This warning is broken on if/else chains with init statements. +#pragma warning(disable : 4456) // declaration of '...' hides previous local declaration + +namespace pcg_engines +{ + /* + * PCG Random Number Generation for C++ + * + * Copyright 2014-2017 Melissa O'Neill , + * and the PCG Project contributors. + * + * SPDX-License-Identifier: (Apache-2.0 OR MIT) + * + * Licensed under the Apache License, Version 2.0 (provided in + * LICENSE-APACHE.txt and at http://www.apache.org/licenses/LICENSE-2.0) + * or under the MIT license (provided in LICENSE-MIT.txt and at + * http://opensource.org/licenses/MIT), at your option. This file may not + * be copied, modified, or distributed except according to those terms. + * + * Distributed on an "AS IS" BASIS, WITHOUT WARRANTY OF ANY KIND, either + * express or implied. See your chosen license for details. + * + * For additional information about the PCG random number generation scheme, + * visit http://www.pcg-random.org/. + */ + + class oneseq_dxsm_64_32 + { + using xtype = uint32_t; + using itype = uint64_t; + + itype state_; + + static constexpr uint64_t multiplier() noexcept + { + return 6364136223846793005ULL; + } + + static constexpr uint64_t increment() noexcept + { + return 1442695040888963407ULL; + } + + static constexpr itype bump(itype state) noexcept + { + return state * multiplier() + increment(); + } + + constexpr itype base_generate0() noexcept + { + itype old_state = state_; + state_ = bump(state_); + return old_state; + } + + public: + explicit constexpr oneseq_dxsm_64_32(itype state = 0xcafef00dd15ea5e5ULL) noexcept : + state_(bump(state + increment())) + { + } + + constexpr xtype operator()() noexcept + { + constexpr auto xtypebits = uint8_t(sizeof(xtype) * 8); + constexpr auto itypebits = uint8_t(sizeof(itype) * 8); + static_assert(xtypebits <= itypebits / 2, "Output type must be half the size of the state type."); + + auto internal = base_generate0(); + auto hi = xtype(internal >> (itypebits - xtypebits)); + auto lo = xtype(internal); + + lo |= 1; + hi ^= hi >> (xtypebits / 2); + hi *= xtype(multiplier()); + hi ^= hi >> (3 * (xtypebits / 4)); + hi *= lo; + return hi; + } + + constexpr xtype operator()(xtype upper_bound) noexcept + { + uint32_t threshold = (UINT64_MAX + uint32_t(1) - upper_bound) % upper_bound; + for (;;) + { + auto r = operator()(); + if (r >= threshold) + return r % upper_bound; + } + } + }; +} // namespace pcg_engines + +template +constexpr T min(T a, T b) +{ + return a < b ? a : b; +} + +template +constexpr T max(T a, T b) +{ + return a > b ? a : b; +} + +template +constexpr T clamp(T val, T min, T max) +{ + return val < min ? min : (val > max ? max : val); +} + +static uint32_t parse_number(const wchar_t* str, const wchar_t** end) noexcept +{ + static constexpr uint32_t max = 0x0fffffff; + uint32_t accumulator = 0; + + for (;; ++str) + { + if (*str == '\0' || *str < '0' || *str > '9') + { + break; + } + + accumulator = accumulator * 10 + *str - '0'; + if (accumulator >= max) + { + accumulator = max; + break; + } + } + + if (end) + { + *end = str; + } + + return accumulator; +} + +static uint32_t parse_number_with_suffix(const wchar_t* str) noexcept +{ + const wchar_t* str_end = nullptr; + auto value = parse_number(str, &str_end); + + if (str_end[0]) + { + uint32_t mul = 1000; + + if (str_end[1]) + { + mul = 1024; + + if (str_end[1] != L'i') + { + value = 0; + } + } + + switch (*str_end) + { + case L'g': + case L'G': + value *= mul; + [[fallthrough]]; + case L'm': + case L'M': + value *= mul; + [[fallthrough]]; + case L'k': + case L'K': + value *= mul; + break; + default: + value = 0; + break; + } + } + + return value; +} + +static bool has_suffix(const wchar_t* str, const wchar_t* suffix) +{ + return wcscmp(str, suffix) == 0; +} + +static const wchar_t* split_prefix(const wchar_t* str, const wchar_t* prefix) noexcept +{ + for (; *prefix; ++prefix, ++str) + { + if (*str != *prefix) + { + return nullptr; + } + } + return str; +} + +static char* buffer_append_long(char* dst, const void* src, size_t size) +{ + memcpy(dst, src, size); + return dst + size; +} + +static char* buffer_append(char* dst, const char* src, size_t size) +{ + for (size_t i = 0; i < size; ++i, ++src, ++dst) + { + *dst = *src; + } + return dst; +} + +static char* buffer_append_string(char* dst, const char* src) +{ + return buffer_append(dst, src, strlen(src)); +} + +char* buffer_append_number(char* dst, uint8_t val) +{ + if (val >= 10) + { + if (val >= 100) + { + const uint8_t d = val / 100; + *dst++ = '0' + d; + val -= d * 100; + } + + const uint8_t d = val / 10; + *dst++ = '0' + d; + val -= d * 10; + } + + *dst++ = '0' + val; + return dst; +} + +struct FormatResult +{ + LONGLONG integral; + LONGLONG fractional; + const char* suffix; +}; + +#define FORMAT_RESULT_FMT "%lld.%03lld%s" +#define FORMAT_RESULT_ARGS(r) r.integral, r.fractional, r.suffix + +static FormatResult format_size(LONGLONG value) +{ + FormatResult result; + if (value >= 1'000'000'000) + { + result.integral = value / 1'000'000'000; + result.fractional = ((value + 500'000) / 1'000'000) % 1000; + result.suffix = "G"; + } + else if (value >= 1'000'000) + { + result.integral = value / 1'000'000; + result.fractional = ((value + 500) / 1'000) % 1000; + result.suffix = "M"; + } + else if (value >= 1'000) + { + result.integral = value / 1'000; + result.fractional = value % 1'000; + result.suffix = "k"; + } + else + { + result.integral = value; + result.fractional = 0; + result.suffix = ""; + } + return result; +} + +static FormatResult format_duration(LONGLONG microseconds) +{ + FormatResult result; + if (microseconds >= 1'000'000) + { + result.integral = microseconds / 1'000'000; + result.fractional = ((microseconds + 500) / 1'000) % 1000; + result.suffix = ""; + } + else + { + result.integral = microseconds / 1'000; + result.fractional = microseconds % 1'000; + result.suffix = "m"; + } + return result; +} + +static int format(char* buffer, int size, const char* format, ...) noexcept +{ + va_list vl; + va_start(vl, format); + const auto length = wvnsprintfA(buffer, size, format, vl); + va_end(vl); + return length; +} + +enum class VtMode +{ + Off, + On, + Italic, + Color +}; + +static HANDLE g_stdout; +static HANDLE g_stderr; +static UINT g_console_cp_old; +static DWORD g_console_mode_old; +static size_t g_large_page_minimum; + +[[noreturn]] static void clean_exit(UINT code) +{ + if (g_console_cp_old) + { + SetConsoleCP(g_console_cp_old); + } + if (g_console_mode_old) + { + SetConsoleMode(g_stdout, g_console_mode_old); + } + ExitProcess(code); +} + +[[noreturn]] static void eprintf(const char* format, ...) noexcept +{ + char buffer[1024]; + + va_list vl; + va_start(vl, format); + const auto length = wvnsprintfA(buffer, sizeof(buffer), format, vl); + va_end(vl); + + if (length > 0) + { + WriteFile(g_stderr, buffer, length, nullptr, nullptr); + } + + clean_exit(1); +} + +[[noreturn]] static void print_last_error(const char* what) noexcept +{ + eprintf("\r\nfailed to %s with 0x%08lx\r\n", what, GetLastError()); +} + +static void acquire_lock_memory_privilege() noexcept +{ + HANDLE token; + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &token)) + { + return; + } + + TOKEN_PRIVILEGES privileges{}; + privileges.PrivilegeCount = 1; + privileges.Privileges[0].Luid = { 4, 0 }; // SE_LOCK_MEMORY_PRIVILEGE is a well known LUID and always {4, 0} + privileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; + + // AdjustTokenPrivileges can return true and still set the last error to ERROR_NOT_ALL_ASSIGNED. This API is nuts... + const bool success = AdjustTokenPrivileges(token, FALSE, &privileges, 0, nullptr, nullptr); + if (success && GetLastError() == S_OK) + { + g_large_page_minimum = GetLargePageMinimum(); + } + + CloseHandle(token); +} + +static char* allocate(size_t size) +{ + if (g_large_page_minimum) + { + const auto large_size = (size + g_large_page_minimum - 1) & ~(g_large_page_minimum - 1); + if (const auto address = static_cast(VirtualAlloc(nullptr, large_size, MEM_COMMIT | MEM_RESERVE | MEM_LARGE_PAGES, PAGE_READWRITE))) + { + return address; + } + } + + if (const auto address = static_cast(VirtualAlloc(nullptr, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE))) + { + return address; + } + + print_last_error("allocate memory"); +} + +static BOOL WINAPI consoleCtrlHandler(DWORD) +{ + CancelIoEx(g_stdout, nullptr); + return TRUE; +} + +int __stdcall main() noexcept +{ + g_stdout = GetStdHandle(STD_OUTPUT_HANDLE); + g_stderr = GetStdHandle(STD_OUTPUT_HANDLE); + g_console_cp_old = GetConsoleOutputCP(); + + SetConsoleCtrlHandler(consoleCtrlHandler, TRUE); + SetConsoleOutputCP(CP_UTF8); + + const wchar_t* path = nullptr; + uint32_t chunk_size = 128 * 1024; + uint32_t repeat = 1; + VtMode vt = VtMode::Off; + uint64_t seed = 0; + bool has_seed = false; + + { + int argc; + const auto argv = CommandLineToArgvW(GetCommandLineW(), &argc); + + for (int i = 1; i < argc; ++i) + { + if (const auto suffix = split_prefix(argv[i], L"-c")) + { + // 1GiB is the maximum buffer size WriteFile seems to accept. + chunk_size = min(parse_number_with_suffix(suffix), 1024 * 1024 * 1024); + } + else if (const auto suffix = split_prefix(argv[i], L"-r")) + { + repeat = parse_number_with_suffix(suffix); + } + else if (const auto suffix = split_prefix(argv[i], L"-v")) + { + vt = VtMode::On; + if (has_suffix(suffix, L"i")) + { + vt = VtMode::Italic; + } + else if (has_suffix(suffix, L"c")) + { + vt = VtMode::Color; + } + else if (*suffix) + { + break; + } + } + else if (has_suffix(argv[i], L"-s")) + { + seed = parse_number_with_suffix(suffix); + has_seed = true; + } + else + { + if (argc - i == 1) + { + path = argv[i]; + } + break; + } + } + } + + if (!path || !chunk_size || !repeat) + { + eprintf( + "bc [options] \r\n" + " -v enable VT\r\n" + " -vi print as italic\r\n" + " -vc print colorized\r\n" + " -c{d}{u} chunk size, defaults to 128Ki\r\n" + " -r{d}{u} repeats, defaults to 1\r\n" + " -s{d} RNG seed\r\n" + "{d} are base-10 digits\r\n" + "{u} are suffix units k, Ki, M, Mi, G, Gi\r\n"); + } + + if (!has_seed && vt == VtMode::Color) + { + const auto cryptbase = LoadLibraryExW(L"cryptbase.dll", nullptr, 0); + if (!cryptbase) + { + print_last_error("get handle to cryptbase.dll"); + } + + const auto RtlGenRandom = reinterpret_cast(GetProcAddress(cryptbase, "SystemFunction036")); + if (!RtlGenRandom) + { + print_last_error("get handle to RtlGenRandom"); + } + + RtlGenRandom(&seed, sizeof(seed)); + } + + pcg_engines::oneseq_dxsm_64_32 rng{ seed }; + + const auto stdout = GetStdHandle(STD_OUTPUT_HANDLE); + const auto file = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + if (file == INVALID_HANDLE_VALUE) + { + print_last_error("open file"); + } + + size_t file_size = 0; + { +#ifdef _WIN64 + LARGE_INTEGER i; + if (!GetFileSizeEx(file, &i)) + { + print_last_error("open file"); + } + + file_size = static_cast(i.QuadPart); + +#else + file_size = GetFileSize(file, nullptr); + if (file_size == INVALID_FILE_SIZE) + { + print_last_error("open file"); + } +#endif + } + + acquire_lock_memory_privilege(); + + const auto file_data = allocate(file_size); + auto stdout_size = file_size; + auto stdout_data = file_data; + + { + auto read_data = file_data; + DWORD read = 0; + + for (auto remaining = file_size; remaining > 0; remaining -= read, read_data += read) + { + read = static_cast(min(0xffffffff, remaining)); + if (!ReadFile(file, read_data, read, &read, nullptr)) + { + print_last_error("read"); + } + } + } + + switch (vt) + { + case VtMode::Italic: + { + stdout_data = allocate(file_size + 16); + auto p = stdout_data; + p = buffer_append_string(p, "\x1b[3m"); + p = buffer_append_long(p, file_data, file_size); + p = buffer_append_string(p, "\x1b[0m"); + stdout_size = static_cast(p - stdout_data); + break; + } + case VtMode::Color: + { + if (const auto icu = LoadLibraryExW(L"icuuc.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32)) + { + stdout_data = allocate(file_size * 20 + 8); + auto p = stdout_data; + + const auto p_utext_openUTF8 = reinterpret_cast(GetProcAddress(icu, "utext_openUTF8")); + const auto p_ubrk_open = reinterpret_cast(GetProcAddress(icu, "ubrk_open")); + const auto p_ubrk_setUText = reinterpret_cast(GetProcAddress(icu, "ubrk_setUText")); + const auto p_ubrk_next = reinterpret_cast(GetProcAddress(icu, "ubrk_next")); + + auto error = U_ZERO_ERROR; + UText text = UTEXT_INITIALIZER; + p_utext_openUTF8(&text, file_data, file_size, &error); + + const auto it = p_ubrk_open(UBRK_CHARACTER, "", nullptr, 0, &error); + p_ubrk_setUText(it, &text, &error); + + for (int32_t ubrk0 = 0, ubrk1; (ubrk1 = p_ubrk_next(it)) != UBRK_DONE; ubrk0 = ubrk1) + { + p = buffer_append_string(p, "\x1b[38;2"); + for (int i = 0; i < 3; i++) + { + *p++ = ';'; + p = buffer_append_number(p, static_cast(rng())); + } + p = buffer_append_string(p, "m"); + p = buffer_append(p, file_data + ubrk0, ubrk1 - ubrk0); + } + + p = buffer_append_string(p, "\x1b[39;49m"); + stdout_size = static_cast(p - stdout_data); + } + break; + } + default: + break; + } + + { + DWORD mode = 0; + if (!GetConsoleMode(g_stdout, &mode)) + { + print_last_error("get console mode"); + } + + g_console_mode_old = mode; + + mode |= ENABLE_PROCESSED_OUTPUT | ENABLE_WRAP_AT_EOL_OUTPUT; + mode &= ~(ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN); + if (vt != VtMode::Off) + { + mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; + } + + if (!SetConsoleMode(g_stdout, mode)) + { + print_last_error("set console mode"); + } + } + + LARGE_INTEGER frequency, beg, end; + QueryPerformanceFrequency(&frequency); + QueryPerformanceCounter(&beg); + + for (size_t iteration = 0; iteration < repeat; ++iteration) + { + auto write_data = stdout_data; + DWORD written = 0; + + for (auto remaining = stdout_size; remaining != 0; remaining -= written, write_data += written) + { + written = static_cast(min(remaining, chunk_size)); + if (!WriteFile(stdout, write_data, written, &written, nullptr)) + { + print_last_error("write"); + } + } + } + + QueryPerformanceCounter(&end); + + const auto elapsed_ticks = end.QuadPart - beg.QuadPart; + const auto elapsed_us = (elapsed_ticks * 1'000'000) / frequency.QuadPart; + LONGLONG total_size = static_cast(stdout_size) * repeat; + const auto bytes_per_second = (total_size * frequency.QuadPart) / elapsed_ticks; + + const auto written = format_size(total_size); + const auto duration = format_duration(elapsed_us); + const auto throughput = format_size(bytes_per_second); + + char status[128]; + const auto status_length = format( + &status[0], + 1024, + FORMAT_RESULT_FMT "B, " FORMAT_RESULT_FMT "s, " FORMAT_RESULT_FMT "B/s", + FORMAT_RESULT_ARGS(written), + FORMAT_RESULT_ARGS(duration), + FORMAT_RESULT_ARGS(throughput)); + + if (status_length <= 0) + { + clean_exit(1); + } + + char buffer[256]; + char* buffer_end = &buffer[0]; + + buffer_end = buffer_append_string(buffer_end, "\r\n"); + for (int i = 0; i < status_length; ++i) + { + *buffer_end++ = '-'; + } + buffer_end = buffer_append_string(buffer_end, "\r\n"); + buffer_end = buffer_append_long(buffer_end, &status[0], static_cast(status_length)); + buffer_end = buffer_append_string(buffer_end, "\r\n"); + + WriteFile(g_stderr, &buffer[0], static_cast(buffer_end - &buffer[0]), nullptr, nullptr); + clean_exit(0); +} diff --git a/src/types/ScreenInfoUiaProviderBase.h b/src/types/ScreenInfoUiaProviderBase.h index 0b9678eacfe..2634d468fa1 100644 --- a/src/types/ScreenInfoUiaProviderBase.h +++ b/src/types/ScreenInfoUiaProviderBase.h @@ -59,13 +59,12 @@ namespace Microsoft::Console::Types IFACEMETHODIMP get_HostRawElementProvider(_COM_Outptr_result_maybenull_ IRawElementProviderSimple** ppProvider) noexcept override; // IRawElementProviderFragment methods - virtual IFACEMETHODIMP Navigate(_In_ NavigateDirection direction, - _COM_Outptr_result_maybenull_ IRawElementProviderFragment** ppProvider) = 0; + IFACEMETHODIMP Navigate(_In_ NavigateDirection direction, _COM_Outptr_result_maybenull_ IRawElementProviderFragment** ppProvider) override = 0; IFACEMETHODIMP GetRuntimeId(_Outptr_result_maybenull_ SAFEARRAY** ppRuntimeId) override; - virtual IFACEMETHODIMP get_BoundingRectangle(_Out_ UiaRect* pRect) = 0; + IFACEMETHODIMP get_BoundingRectangle(_Out_ UiaRect* pRect) override = 0; IFACEMETHODIMP GetEmbeddedFragmentRoots(_Outptr_result_maybenull_ SAFEARRAY** ppRoots) noexcept override; IFACEMETHODIMP SetFocus() override; - virtual IFACEMETHODIMP get_FragmentRoot(_COM_Outptr_result_maybenull_ IRawElementProviderFragmentRoot** ppProvider) = 0; + IFACEMETHODIMP get_FragmentRoot(_COM_Outptr_result_maybenull_ IRawElementProviderFragmentRoot** ppProvider) override = 0; // ITextProvider IFACEMETHODIMP GetSelection(_Outptr_result_maybenull_ SAFEARRAY** ppRetVal) override; diff --git a/src/types/UiaTextRangeBase.hpp b/src/types/UiaTextRangeBase.hpp index 74ff7f0e160..c69ff5b4a73 100644 --- a/src/types/UiaTextRangeBase.hpp +++ b/src/types/UiaTextRangeBase.hpp @@ -82,7 +82,7 @@ namespace Microsoft::Console::Types bool IsDegenerate() const noexcept; // ITextRangeProvider methods - virtual IFACEMETHODIMP Clone(_Outptr_result_maybenull_ ITextRangeProvider** ppRetVal) = 0; + IFACEMETHODIMP Clone(_Outptr_result_maybenull_ ITextRangeProvider** ppRetVal) override = 0; IFACEMETHODIMP Compare(_In_opt_ ITextRangeProvider* pRange, _Out_ BOOL* pRetVal) noexcept override; IFACEMETHODIMP CompareEndpoints(_In_ TextPatternRangeEndpoint endpoint, _In_ ITextRangeProvider* pTargetRange, diff --git a/src/types/colorTable.cpp b/src/types/colorTable.cpp index 46f59bfd2dd..f21b8586217 100644 --- a/src/types/colorTable.cpp +++ b/src/types/colorTable.cpp @@ -267,7 +267,7 @@ static constexpr std::array standard256ColorTable{ til::color{ 0xEE, 0xEE, 0xEE }, }; -static constexpr til::presorted_static_map xorgAppVariantColorTable{ +static constinit til::presorted_static_map xorgAppVariantColorTable{ std::pair{ "antiquewhite"sv, std::array{ til::color{ 250, 235, 215 }, til::color{ 255, 239, 219 }, til::color{ 238, 223, 204 }, til::color{ 205, 192, 176 }, til::color{ 139, 131, 120 } } }, std::pair{ "aquamarine"sv, std::array{ til::color{ 127, 255, 212 }, til::color{ 127, 255, 212 }, til::color{ 118, 238, 198 }, til::color{ 102, 205, 170 }, til::color{ 69, 139, 116 } } }, std::pair{ "azure"sv, std::array{ til::color{ 240, 255, 255 }, til::color{ 240, 255, 255 }, til::color{ 224, 238, 238 }, til::color{ 193, 205, 205 }, til::color{ 131, 139, 139 } } }, @@ -348,7 +348,7 @@ static constexpr til::presorted_static_map xorgAppVariantColorTable{ std::pair{ "yellow"sv, std::array{ til::color{ 255, 255, 0 }, til::color{ 255, 255, 0 }, til::color{ 238, 238, 0 }, til::color{ 205, 205, 0 }, til::color{ 139, 139, 0 } } }, }; -static constexpr til::presorted_static_map xorgAppColorTable{ +static constinit til::presorted_static_map xorgAppColorTable{ std::pair{ "aliceblue"sv, til::color{ 240, 248, 255 } }, std::pair{ "aqua"sv, til::color{ 0, 255, 255 } }, std::pair{ "beige"sv, til::color{ 245, 245, 220 } }, diff --git a/src/types/inc/IInputEvent.hpp b/src/types/inc/IInputEvent.hpp index 50626cdda7e..8e5dfb0afd7 100644 --- a/src/types/inc/IInputEvent.hpp +++ b/src/types/inc/IInputEvent.hpp @@ -515,14 +515,12 @@ class FocusEvent : public IInputEvent { public: constexpr FocusEvent(const FOCUS_EVENT_RECORD& record) : - _focus{ !!record.bSetFocus }, - _cameFromApi{ true } + _focus{ !!record.bSetFocus } { } constexpr FocusEvent(const bool focus) : - _focus{ focus }, - _cameFromApi{ false } + _focus{ focus } { } @@ -542,15 +540,8 @@ class FocusEvent : public IInputEvent void SetFocus(const bool focus) noexcept; - // BODGY - see FocusEvent.cpp for details. - constexpr bool CameFromApi() const noexcept - { - return _cameFromApi; - } - private: bool _focus; - bool _cameFromApi; #ifdef UNIT_TESTING friend std::wostream& operator<<(std::wostream& stream, const FocusEvent* const pFocusEvent); diff --git a/src/winconpty/winconpty.cpp b/src/winconpty/winconpty.cpp index e964a5691a3..323e4f788f9 100644 --- a/src/winconpty/winconpty.cpp +++ b/src/winconpty/winconpty.cpp @@ -133,19 +133,17 @@ HRESULT _CreatePseudoConsole(const HANDLE hToken, RETURN_IF_WIN32_BOOL_FALSE(SetHandleInformation(signalPipeConhostSide.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT)); // GH4061: Ensure that the path to executable in the format is escaped so C:\Program.exe cannot collide with C:\Program Files - auto pwszFormat = L"\"%s\" --headless %s%s%s%s--width %hu --height %hu --signal 0x%x --server 0x%x"; + auto pwszFormat = L"\"%s\" --headless %s%s%s--width %hu --height %hu --signal 0x%x --server 0x%x"; // This is plenty of space to hold the formatted string wchar_t cmd[MAX_PATH]{}; const BOOL bInheritCursor = (dwFlags & PSEUDOCONSOLE_INHERIT_CURSOR) == PSEUDOCONSOLE_INHERIT_CURSOR; const BOOL bResizeQuirk = (dwFlags & PSEUDOCONSOLE_RESIZE_QUIRK) == PSEUDOCONSOLE_RESIZE_QUIRK; - const BOOL bWin32InputMode = (dwFlags & PSEUDOCONSOLE_WIN32_INPUT_MODE) == PSEUDOCONSOLE_WIN32_INPUT_MODE; const BOOL bPassthroughMode = (dwFlags & PSEUDOCONSOLE_PASSTHROUGH_MODE) == PSEUDOCONSOLE_PASSTHROUGH_MODE; swprintf_s(cmd, MAX_PATH, pwszFormat, _ConsoleHostPath(), bInheritCursor ? L"--inheritcursor " : L"", - bWin32InputMode ? L"--win32input " : L"", bResizeQuirk ? L"--resizeQuirk " : L"", bPassthroughMode ? L"--passthrough " : L"", size.X, @@ -327,15 +325,20 @@ HRESULT _ReparentPseudoConsole(_In_ const PseudoConsole* const pPty, _In_ const { return E_INVALIDARG; } + // sneaky way to pack a short and a uint64_t in a relatively literal way. #pragma pack(push, 1) struct _signal { const unsigned short id; const uint64_t hwnd; - } data{ PTY_SIGNAL_REPARENT_WINDOW, (uint64_t)(newParent) }; + }; #pragma pack(pop) + const _signal data{ + PTY_SIGNAL_REPARENT_WINDOW, + (uint64_t)(newParent), + }; const auto fSuccess = WriteFile(pPty->hSignal, &data, sizeof(data), nullptr, nullptr); return fSuccess ? S_OK : HRESULT_FROM_WIN32(GetLastError()); diff --git a/tools/ConsoleTypes.natvis b/tools/ConsoleTypes.natvis index 79a2fe7abd0..ba87bc3a450 100644 --- a/tools/ConsoleTypes.natvis +++ b/tools/ConsoleTypes.natvis @@ -10,16 +10,8 @@ - - {{FG: {_foreground}, BG: {_background}, Legacy: {_wAttrLegacy}, {_extendedAttrs}} - - _wAttrLegacy - _foreground - _background - _extendedAttrs - + {{FG: {_foreground}, BG: {_background}, Attr: {_attrs}} + {{FG: {_foreground}, BG: {_background}, Attr: {_attrs}, Hyperlink: {_hyperlinkId}} @@ -59,15 +51,15 @@ - {{W: {_width,d} x H: {_height,d} -> A: {_width * _height, d}}} + {{W: {width} x H: {height} -> A: {width * height}}} - {{X: {_x,d}, Y: {_y,d}}} + {{X: {x}, Y: {y}}} - {{L: {_topLeft._x}, T: {_topLeft._y}, R: {_bottomRight._x} B: {_bottomRight._y} [W: {_bottomRight._x - _topLeft._x} x H: {_bottomRight._y - _topLeft._y} -> A: {(_bottomRight._x - _topLeft._x) * (_bottomRight._y - _topLeft._y)}]}} + {{L: {left}, T: {top}, R: {right} B: {bottom} [W: {right - left} x H: {bottom - top} -> A: {(right - left) * (bottom - top)}]}} @@ -122,6 +114,18 @@ + + {{ width={_width}, height={_height} }} + + _firstRow + *(ROW*)_buffer.__ptr_.__value_ + + (_commitWatermark - _buffer.__ptr_.__value_ - 1) / _bufferRowStride + *(ROW*)(_buffer.__ptr_.__value_ + _bufferRowStride * ($i + 1)) + + + + {{ size={_size} }} diff --git a/tools/OpenConsole.psm1 b/tools/OpenConsole.psm1 index fd0ededc112..3dd17957161 100644 --- a/tools/OpenConsole.psm1 +++ b/tools/OpenConsole.psm1 @@ -367,7 +367,7 @@ function Test-XamlFormat() { $xamlsForStyler = (git ls-files "$root/**/*.xaml") -join "," dotnet tool run xstyler -- -c "$root\XamlStyler.json" -f "$xamlsForStyler" --passive - if ($lastExitCode -eq 1) { + if ($LASTEXITCODE -eq 1) { throw "Xaml formatting bad, run Invoke-XamlFormat on branch" }